Shadows, borders and corners
beginner·8 min·Markdown for agents
What this lesson teaches
Lift surfaces with shadows that whisper, separate them with borders, nest rounded corners correctly, and trust your eye over the ruler.
Four ways to lift the same card. Start on "The default", then try "Whisper".
Mira Okafor
Commented on Homepage v3
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
Not wrong, just everywhere. One flat blur at 10% reads as a template.
0 2px 8px rgba(0, 0, 0, 0.1) is the most copied shadow on the web. Nothing is wrong with it except that it is everywhere, so it reads as "template".
Shadows should whisper
A shadow's job is to hint that a surface floats, which designers call . If you can clearly see a soft gray blur, it's too strong.
The fix is a : several faint shadows stacked in one declaration.
--shadow-md:
0 1px 2px rgba(17, 17, 17, 0.05),
0 4px 8px rgba(17, 17, 17, 0.05),
0 8px 24px rgba(17, 17, 17, 0.04);What makes it work:
- Several layers at different blurs (2px, 8px, 24px) read as physical depth. One big blur looks flat.
- Opacity at 4–6%, not 10%. This is the part most people get wrong.
- A near-black base like
#111, not pure black. - The y-offset never bigger than the blur.
Build at least three, for three heights: cards (+1), dropdowns and popovers (+2), dialogs (+3).
Shadows raise; borders separate
Pick by the job the line is doing.
- Raised things (cards, menus, popovers, dialogs) get a shadow. Because a shadow is see-through, one token works over any background.
- Separated things (table rows, list dividers, input edges) keep a border. A visible input edge is an accessibility feature.
The rule is about elevation, not every line. An agent that reads "use shadows instead of borders" and strips the edges off inputs and table cells has made the UI worse. And use one depth cue per surface: border plus shadow plus ring plus gradient on one card is hedging.
A few finishing lines from Emil Kowalski's practice:
- at 0.5px on high-density screens, 1px elsewhere. Retina dividers at 1px look heavy.
- Every image gets a 1px inset outline in pure black (or white in dark) at 10%. A tinted gray picks up the background and looks like grime.
- Fade with
mask-image, not a gradient laid on top, and never fade content people are scrolling toward.
Corners: a scale, and the nesting rule
is a personality dial. The same radius on everything is lazy; a small scale used on purpose looks crafted.
--radius-sm: 4px; /* badges */
--radius-md: 8px; /* inputs, small buttons */
--radius-lg: 12px; /* cards */
--radius-xl: 16px; /* modals */
--radius-full: 9999px; /* pills, avatars */Mixing far-apart sizes (4px badges in a 16px modal) is fine. Mixing near ones (4px and 8px side by side) looks like a mistake. And if a button is a pill, everything inside it commits to the pill too.
Now the rule people miss most. When a rounded box sits inside another, the inner radius isn't the same as the outer one. It's smaller, by the gap:
inner radius = outer radius − padding
inner = 20px (copied)With equal radii, the gap between the two curves gets thinner at each corner and the inner box seems to bulge. With the subtraction, the curves run parallel. Copying the parent's radius is the single most frequent giveaway of an unpolished UI.
Trust your eye over the ruler
Perfect math sometimes looks wrong. A triangle centered by its bounding box looks shoved left, because most of its box is empty. A circle the same height as a square looks smaller, because it doesn't fill its corners.
The fix is : nudge until it looks right. The play glyph moves a pixel or two right; the circle grows about 5%. If it looks wrong, it is wrong.
The same idea, a little imperfection on purpose, extends further on marketing pages: a faint paper-grain texture instead of a flat fill, a hand-drawn stroke, an asymmetric layout that feels composed. Keep it there. Inside the product (forms, tables, settings) consistency wins, and "imperfect" just means "broken".
Review the surfaces in this component: - Replace any single-blur box-shadow with our layered tokens (--shadow-sm/md/lg: 2–3 layers, 4–6% opacity, base #111, y-offset ≤ blur). - Shadows only on raised surfaces (cards, menus, dialogs). Keep borders on inputs, table cells and dividers. - One depth cue per surface. - Nested rounded elements: inner radius = outer radius − padding. - Images get outline: 1px solid rgb(0 0 0 / .1) with outline-offset: -1px. Return a Before | After | Why table.
Quick check
A dialog has border-radius 16px and padding 6px. What radius should the panel inside it get?
In the skill
The nodes your agent reads when it does this work.
- Shadows whisperShadows should whisper, not announce. The default 0 2px 8px is a tell. Concrete spec.
surface/shadows-whisper - Depth and nestingInner radius = outer − padding. Shadows elevate, borders separate. Dark mode collapses shadows to a ring. Hairlines at 0.5px on retina, image outlines pure black or white at 10%, fades by mask, themes by flipping variables.
surface/depth-and-nesting - Border radiusUniform radius everywhere is lazy. Capsule buttons need full package. Nested-radius rule.
surface/border-radius - Visual imperfectionOptical alignment beats mathematical. Texture and noise beat flat. Imperfect shapes beat perfect geometry — in marketing.
surface/visual-imperfection
Connected lessons
What this lesson builds on and leads to, across every course.