Skip to content

Foundations

Borders

This system draws its structure with 1px lines rather than shadows. A border is precise, costs nothing to render, and keeps a page of cards looking flat and calm instead of hovering.

The weightsPermalink to this section

Four values, only three of which appear at rest. Every border in the system is 1px. Weight is expressed through colour, not thickness.

--border-subtle

#efeae4

Dividers inside a single component

Rows in a list, the rule above a card footer

--border-default

#e3dcd4

The default for everything

Cards, inputs, buttons, table headers

--border-strong

#ccc3b9

Hover emphasis and separators that must read

Input hover, outline button hover

--border-focus

#c94e0c

Focus only

The focus ring, and focused input borders

Borders instead of shadowsPermalink to this section

Both can define a surface. A border does it without implying that the surface is floating, which is the right default for content that is simply on the page.

Q3 marketing site

Bordered, flat, calm.

Dodefine cards with a border

The card is clearly a unit, and a page of twelve of them stays quiet.

Q3 marketing site

Floating for no reason.

Don'televate everything

If every surface casts a shadow, the ones that genuinely overlay the page, such as dialogs and menus, have nothing left to distinguish them.

Where each weight goesPermalink to this section

The distinctions that come up in review.

ContextWeightWhy
Card outlinedefaultThe card's boundary against the canvas
Rows inside a cardsubtleA separation, not a boundary. It should barely register
Input at restdefaultMatches the buttons beside it
Input on hoverstrongConfirms the field is interactive before it is clicked
Input on focusfocusBrand colour plus a 3px ring
Table headerdefaultSeparates the header from the body
Table rowssubtleEnough to track across a row, not enough to stripe the page
Section dividersubtleBetween documentation sections

Directional gradient bordersPermalink to this section

Use an edge frame when a feature surface needs a softer boundary than a complete box. One edge stays legible, a short highlight creates focus, and the remaining perimeter fades into the surface.

edge="top"

top focus

The frame dissolves as it moves away from the emphasized edge.

edge="right"

right focus

The frame dissolves as it moves away from the emphasized edge.

edge="bottom"

bottom focus

The frame dissolves as it moves away from the emphasized edge.

edge="left"

left focus

The frame dissolves as it moves away from the emphasized edge.

effect="glow"

Colored edge light

Set any CSS color on the sharp highlight. The two blurred layers inherit it automatically and stay clipped inside the frame.

feature.tsx
import { EdgeFrame } from "@/components/ui/edge-frame";

<EdgeFrame edge="top" className="p-6">
  <h3>Focused boundary</h3>
  <p>The remaining perimeter fades into the surface.</p>
</EdgeFrame>

Decorative, not structural

A fading perimeter can disappear in places, so do not use it as the only boundary for an input, button, or other interactive control. Those controls still need a complete 3:1 identifying boundary or another equally clear visual affordance.

HoverPermalink to this section

Darkening a border from default to strong is the system's quietest way of saying “this responds”. It is used on every bordered interactive surface.

Hover this card

ContrastPermalink to this section

Borders that are the only thing defining an interactive control have to meet the non-text contrast requirement.

3:1 for functional borders

WCAG 2.2 requires 3:1 for boundaries that identify a control. The default border is 1.4:1 against white, which is fine for a card. The card is not a control. Input borders pass because the field also carries a label, a fill difference, and a focus treatment that clears 3:1 on its own.

BorderAgainstRatioAcceptable because
border-subtlebackground1.1:1Decorative divider only
border-defaultbackground1.4:1Card boundary, not a control identifier
border-strongbackground1.7:1Hover emphasis, paired with a cursor change
border-focusbackground4.6:1The focus indicator clears 3:1 comfortably

In codePermalink to this section

A base rule sets the default border colour globally, so `border` alone produces the right line without repeating the colour utility everywhere.

src/app/globals.css
@layer base {
  /* Tailwind v4 defaults borders to currentColor. Setting the
     system default here means `border` alone is always correct. */
  *,
  *::before,
  *::after {
    border-color: var(--border-default);
  }
}