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.
Do — define 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't — elevate 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.
| Context | Weight | Why |
|---|---|---|
| Card outline | default | The card's boundary against the canvas |
| Rows inside a card | subtle | A separation, not a boundary. It should barely register |
| Input at rest | default | Matches the buttons beside it |
| Input on hover | strong | Confirms the field is interactive before it is clicked |
| Input on focus | focus | Brand colour plus a 3px ring |
| Table header | default | Separates the header from the body |
| Table rows | subtle | Enough to track across a row, not enough to stripe the page |
| Section divider | subtle | Between 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.
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.
| Border | Against | Ratio | Acceptable because |
|---|---|---|---|
| border-subtle | background | 1.1:1 | Decorative divider only |
| border-default | background | 1.4:1 | Card boundary, not a control identifier |
| border-strong | background | 1.7:1 | Hover emphasis, paired with a cursor change |
| border-focus | background | 4.6:1 | The 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.
@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);
}
}