Overview.
Borders do four things in Flatpay’s product surfaces. They contain a card or input, they divide rows and sections, they signal state (selected, focused, danger), and they framemedia. They never decorate. A line earns its pixel by communicating one of those four things, or it isn’t drawn.
The whole system rides on three width tokens and one neutral colour. Status borders inherit from the same colour ramps as text and surface — they aren’t their own palette.
Borders before shadows
A surface in the page’s normal flow earns a 1 px border, not a shadow. Reach for shadow only when the surface has to read as floating — popovers, modals, drag previews. See Elevation for the rule in detail.
Width.
Two widths run the system. 1 px is the default for everything; 2 px is reserved for the moments where a line has to announce itself — selected items and keyboard focus. There is no third width.
border.width
1 px
Default
Cards, inputs, dividers, list rows, table cells, status pills. Pretty much every surface that needs an edge.
border.width.selected
2 px
Selected · Focused
The active tab, a chosen radio card, the keyboard-focused control. Always paired with a colour that's distinct from the default border.
Why no thicker option
Anything thicker than 2 px stops reading as a border and starts reading as decoration. If a surface needs more emphasis, change the surface tint or the type weight — never thicken the line.
Color.
One neutral colour does the structural work. It tints with the surface — a touch warmer on light, a touch cooler on dark — and inherits from the same --border token wired in @flatpay-dk/ui/styles. Status borders (danger, success, warning) borrow from the matching text ramp; they aren’t a separate palette.
Default · Light
The --border token in light mode — charcoal-tinted neutral on off-white. The default border for cards, inputs, and dividers.
Default · Dark
The same --border token in dark mode — a low-chroma neutral on charcoal. The token re-themes; the consumer never names a colour.
Status · Danger
The --border-danger token — red-700 on light, red-500 on dark. Used on a field that failed validation, paired with the same hue in the helper text.
One neutral, four status hues
Status borders only enter the interface when a state is being communicated: danger (red), success (green), warning (yellow), info (blue). They are never decorative. The default border stays mono in every other case — the surface or the type carries the colour.
Focus ring.
Keyboard focus is a 2 px outline with a 2 px offset, drawn around the element rather than on top of its border. The offset keeps the underlying border legible and lets the ring read on every surface — light, dark, brand. Never replace the outline with a colour change inside the element; users navigating without a mouse rely on it.
Button
Input
Tab
Use :focus-visible, not :focus
Apply the ring with :focus-visible so it shows for keyboard users without flashing on every mouse click. Tailwind ships focus-visible: variants that map to the same selector.
Dividers.
A divider is a 1 px border on one edge — top, bottom, left, or right — inside an otherwise borderless container. Dividers separate siblings(rows in a list, columns in a stat bar). They never wrap the parent; that’s a containment border, not a divider.
- Demo ready
flatpay/lucky-garden
Derek Fidler
- Building
flatpay/cashier-iq
Mira Holm
- Archived
flatpay/receipts-rx
Lukas Berg
Prototypes
42
Demo ready
11
Building
23
Archived
8
In code.
The --border CSS variable is set globally in @flatpay-dk/ui/styles and applied to every element via * { border-color: hsl(var(--border)); }. That means every Tailwind border and divide utility picks up the token without further configuration.
tsx
// Default — 1px containment border, 8px radius
<div className="rounded-lg border bg-card px-4 py-3" />
// Selected — 2px focus-coloured border
<button className="rounded-sm border-2 border-foreground bg-background px-3 py-1.5" />
// Focus ring — 2px outline with 2px offset
<button className="rounded-sm bg-foreground px-4 py-2 text-background
focus-visible:outline-2 focus-visible:outline-offset-2
focus-visible:outline-[var(--border-focus)]" />
// Dividers — between list rows
<ul className="divide-y rounded-lg border bg-card">
<li className="px-5 py-4">…</li>
<li className="px-5 py-4">…</li>
</ul>Don’ts.
Three patterns the lab refuses to ship. Each one shows up in drafts often enough to keep here as a reference.
Heads up
Manifest is missing a maintainer.
Don't side-stripe. A 3–4 px coloured border on one edge is the most recognisable AI-design tell on the planet — admin, dashboard, and medical UIs alike. Use a background tint and an eyebrow label instead.
flatpay/lucky-garden
Demo ready · 12 commits
Don't thicken the border for emphasis. A 3 px border reads as decoration, not structure. If a surface needs more weight, change the surface tint or the type weight.
Related.
Page history
1 revision- DocumentedDerek Fidler@derekfidler
First documented version. Width / color / focus / dividers / misuse, modeled on Atlassian's Border foundation but tightened to Flatpay's editorial restraint.