The defaults for all three of these produce something recognisably machine-made. The fixes are small and each has a physical reason behind it.
Why two-stop gradients look muddy
A gradient from blue to yellow passes through grey, not green. That is not a bug in the browser — it is what interpolating between two RGB values does.
Blue is roughly (0,0,255) and yellow (255,255,0). The midpoint is (128,128,128), which is grey. Any two colours on opposite sides of the wheel produce a desaturated band in the middle.
- Add a midpoint stop in the hue you want to pass through. Three stops fixes most of it.
- Or interpolate in a better space:
linear-gradient(in oklch, blue, yellow)keeps saturation through the transition. Browser support is now broad. - Keep hues close. The most convincing gradients move 30–60 degrees of hue, not 180.
- Vary lightness as well as hue. A gradient that only changes hue at constant lightness reads as flat.
The other giveaway is banding on large areas — visible steps in a smooth gradient, worst on dark colours across a wide element. Adding a very subtle noise overlay hides it, which is what most design tools do behind the scenes.
Shadows have two parts
A single box-shadow with a large blur is the default and looks like a sticker with a grey halo. Real shadows are two things at once.
There is a contact shadow — tight, dark, close to the object, from light blocked immediately at the join. And an ambient shadow — wide, very soft, much lighter, from diffuse light in the room.
Layering both is the whole trick: box-shadow: 0 1px 2px rgba(0,0,0,.10), 0 8px 24px -8px rgba(0,0,0,.18). The first keeps the element attached to the surface; the second gives it height. A single shadow can do one or the other and never both.
Shadow rules that hold
| Rule | Why |
|---|---|
| Blur roughly twice the y-offset | Matches how light actually falls off |
| Negative spread on the wide layer | Stops it pooling around the edges |
| Never pure black | Real shadows carry the surface hue |
| One light source per page | Mixed directions read as a mistake |
| Bigger elevation, softer and larger | A modal casts more than a button |
| Reduce opacity in dark mode | A black shadow on dark is invisible |
That last row catches people converting a light design. Shadows do almost no work on a dark background — depth there comes from a lighter surface rather than a darker shadow, which is a different mental model rather than an inverted one.
Building a palette from one colour
The common approach — take a hue and rotate it 30 degrees for each new colour — produces a set that looks related and unbalanced, because equal hue steps are not equal perceptual steps.
It is also the reason a palette built by holding HSL lightness constant does not look evenly weighted: yellow at 50% lightness is dramatically brighter than blue at 50%. HSL lightness is mathematical, not perceptual.
- Start with one brand hue and build a lightness ramp from it — five to nine steps from very light to very dark. That single ramp does most of the work in any interface.
- Add a neutral ramp tinted very slightly toward the brand hue. Pure grey next to a warm brand colour looks dead.
- Add semantic colours for success, warning and error, adjusted to sit at similar perceived lightness to each other.
- Use OKLCH if you can. Equal lightness values genuinely look equally light, which is the thing HSL cannot give you.
- Check every text pair for contrast as you go, not at the end.
That last step is not optional and it is where palettes fail late. A ramp that looks balanced can put body text at 3.8:1, and you will not see it by eye — the reasoning is in WCAG contrast requirements.
Glass effects, cheaply
Frosted glass is backdrop-filter: blur() plus a translucent background, and it is expensive — the browser composites and blurs everything behind the element on every frame.
On a large area, or on several elements at once, it is one of the most reliable ways to make a page feel slow on a mid-range phone.
- Keep the blurred area small. A header bar or a card, not a full-page overlay.
- Blur radius 8–12px is plenty. Larger costs more and looks no better.
- Always set a background colour too.
backdrop-filteris unsupported in some contexts and the element then has no ground at all. - Add a hairline border at low opacity. Real glass has an edge, and without it the panel looks like a smudge.
- Test the text contrast against the busiest thing that can scroll under it, not against the calm part of the page.
That final point is the accessibility trap in the whole effect: contrast is measured against a background that moves. A design checked against one screenshot can fail badly two scroll positions later.
Frequently asked questions
Why does my gradient look grey in the middle?
Because interpolating between two RGB values passes through their arithmetic midpoint, and for colours on opposite sides of the wheel that is grey. Add a midpoint stop in the hue you want, or use linear-gradient(in oklch, …) which keeps saturation through the transition.
Why does my box-shadow look fake?
It is one layer. Real shadows have a tight dark contact shadow plus a wide soft ambient one. Layer both — a small tight shadow keeps the element attached to the surface, a larger soft one gives it height. One shadow can do either but not both.
Should shadows be pure black?
No. Real shadows carry the hue of the surface they fall on, so a slightly tinted dark colour at low opacity looks right where pure black looks like a hole. In dark mode, reduce opacity heavily — shadows do almost no work there.
How do I build a colour palette from one brand colour?
Build a lightness ramp of five to nine steps from that hue, add a neutral ramp tinted slightly toward it, then semantic colours at similar perceived lightness. Rotating hue in equal steps produces something that looks related and unbalanced.
Why do my colours look uneven at the same lightness?
HSL lightness is mathematical rather than perceptual — yellow at 50% is far brighter than blue at 50%. OKLCH fixes this: equal lightness values genuinely look equally light, which is what makes cross-hue palettes work.
Is backdrop-filter bad for performance?
It is expensive — the browser composites and blurs everything behind the element every frame. Keep the area small, use a blur radius of 8-12px, and always set a fallback background colour, since the property is unsupported in some contexts.