Shape
How solid a surface looks: one radius choice that produces two values, whether anything is outlined, and the three shadows that are the entire vocabulary.
Radius
rounded-nonerounded-xs rounded-smrounded-md rounded-xlrounded-full rounded-2xlElevation
no shadowshadow-smshadow-sm hover:shadow-mdRadius, borders and elevation are conventions, not tokens. Tailwind already ships every
utility any of them needs, so the only decision left is which ones this project uses — and that
decision is made once in the theme builder and written into the generated
theme-rules.md. Nothing on this page is a custom class you have to learn.
Radius
| How round is the shape language? | What it means | Classes |
|---|---|---|
| Sharp | Square corners everywhere. | rounded-none |
| Subtle | Barely rounded — 2px on controls, 4px on surfaces. | rounded-xsrounded-sm |
| Soft shipped default | Rounded — 6px on controls, 12px on surfaces. | rounded-mdrounded-xl |
| Round | Generous — pill buttons, 16px surfaces. | rounded-fullrounded-2xl |
One choice, two values. Picking a radius does not mean picking a card radius and then a button radius. Each option carries a control value and a surface value, and they move together:
| Tier | Class | Applies to |
|---|---|---|
| Control | rounded-md | Buttons, inputs, chips, tabs, badges |
| Surface | rounded-xl | Cards, panels, images, dialogs |
This is real in code, not a description of one. controlClasses() reads classes[0] and
cardClasses() reads .at(-1), both in src/lib/theme-rules.mjs, so a button and the card it
sits inside cannot disagree about the shape language.
Borders
| Do surfaces get outlines? | What it means | Classes |
|---|---|---|
| No borders — zone with grounds shipped default | Surfaces separate by sitting on adjacent near-tones. No hairlines. | none |
| Hairline borders | A 1px ink border at 12% on cards and inputs. | border border-dark/12 |
The shipped choice is no borders: surfaces separate by sitting on adjacent near-tones instead. A white card on the light page ground is already a boundary, and a hairline on top of it is a second statement of the same fact. See Color → Zoning without borders for the grounds that do the work.
Ink is invisible on ink. When a border is used, it flips ladders on an inverse band —
border-white/12, never border-dark/12. cardClasses() swaps the prefix rather than
maintaining two class strings, so a card and its inverse twin cannot drift apart.
Reach for a hairline only when two surfaces must sit on the same ground and genuinely cannot be told apart. Never invent an opacity step outside the ladder.
Elevation
| Do raised surfaces cast shadows? | What it means | Classes |
|---|---|---|
| Flat shipped default | No shadows. Depth comes from grounds. | none |
| Resting only | `shadow-sm` on raised surfaces; nothing on hover. | shadow-smshadow-xl |
| Lift on hover | `shadow-sm` at rest, `shadow-md` on hover. | shadow-smhover:shadow-mdshadow-xl |
Three levels is the entire vocabulary — nothing, resting, and overlay. There is no
shadow-lg or shadow-2xl in this system, and adding one does not add a level of hierarchy; it
adds an argument about which of two nearly identical shadows a given card deserves.
| Class | Where |
|---|---|
shadow-sm | A raised surface at rest, if the project uses shadows at all |
hover:shadow-md | Lift on hover — only in the lift option, never at rest |
shadow-xl | An overlay above a scrim. Available in every option, including flat |
shadow-md at rest reads as a stuck hover state. It is the shadow a user’s eye has learned
to expect during an interaction, so a card wearing it permanently looks like it is waiting for
a click that already happened.
A shadow needs an opaque object to fall away from. Outlined and ghost controls are never
raised — under a transparent control you see the shadow with no edge above it, which reads as a
rendering bug rather than as depth. controlClasses() takes raised as an argument for exactly
this reason, and Button passes it only for filled variants.
Layering
Stacking order is not a per-project choice. A toast that appears underneath a modal is wrong
at every density, under every brand — so this is a fixed ladder, like the focus ring, and the
theme builder does not offer a vote on it. Every rung is one of Tailwind’s own z-* steps.
| Layer | What sits there |
|---|---|
z-10 | Sticky headers, sticky table columns |
z-20 | Dropdowns, popovers, tooltips |
z-30 | The scrim behind a dialog |
z-40 | Modals and drawers — above their own scrim |
z-50 | Toasts — above everything, including a modal |
z-30 is the scrim rather than the dialog because a modal has to clear its own backdrop.
Anything that must sit above a dialog and below a toast has no rung — that is the point, not an
omission.
Rules
- Two radius values, not two decisions. Controls take the first, surfaces take the last. Never pick a corner radius for one component.
- Do not draw a border where a ground change will do. Alternate
bg-lightandbg-whitebands; reach for a hairline only when two surfaces share a ground. - Flip the hairline on inverse bands —
border-white/12. Ink on a dark ground is invisible. - Three shadows, no more.
shadow-smat rest,hover:shadow-mdonly if the project lifts,shadow-xlonly for overlays above a scrim. - Never
shadow-mdat rest. It reads as a stuck hover state. - Never raise a transparent control. Outlined and ghost buttons cast no shadow.
- Do not add a
z-indexthat is not on the ladder. An element that needs one is overlapping something; if it is not in the table above, reorder the markup instead. - Do not turn any of this into a token.
--radius-cardand--shadow-1invent a bespoke surface for a valuerounded-xlandshadow-smalready carry. If Tailwind ships the utility, it is a convention.