Learning Track
Filter content by your role to see the most relevant sections
Border & Stroke Foundations
Frame and delineate with tokens: hairline and thick widths, three stroke styles, semantic border color roles with per-mode resolution, and the non-text contrast contract that keeps meaningful borders visible.
Why This Matters
Borders are the quietest workhorse in the visual system. They delineate regions, signal interactivity, separate data, and carry focus—and because they are quiet, their drift is invisible until the interface feels made of parts that do not know each other: a 1px card beside a 2px input beside a borderless chip, each fine, together noise.
Borders also carry one of the hardest accessibility contracts in the system. WCAG 1.4.11 requires non-text contrast: if a border is the only thing distinguishing a control or state, it must hit 3:1 against its adjacent colors. A perfectly on-brand hairline that vanishes on tinted surfaces is not a style problem—it is a compliance defect, and it is per-mode: the pair that passes in light can fail in dark.
This page covers the border system as built here: widths and styles in shape.border, semantic defaults in the control tokens, and the border color roles in the semantic color layer.
Core Concepts: Widths, Styles, Roles, Contrast
Two Widths, Three Styles
The core layer in ui/designTokens/core/shape.tokens.json keeps the vocabulary deliberately tiny:
// shape.border (values)
width: hairline: 1px thick: 2px
style: solid | dashed | dottedTwo widths because borders have exactly two jobs: separate (hairline) and emphasize (thick). The moment a system adds a 1.5px "just between" width, every border becomes a negotiation. Three styles because each is semantic: solid is structure, dashed is impermanence (placeholders, drag targets, unsaved state), dotted is abbreviation (truncation hints, abstracted content). A dashed input border is not a style choice; it is a sentence about that input.
Semantic Defaults and the Focus Width
The semantic control layer composes the core pieces into the contract components actually consume:
// ui/designTokens/semantic/shape.tokens.json (excerpt)
"control": {
"border": {
"defaultWidth": { "$value": "{shape.border.width.hairline}" },
"defaultStyle": { "$value": "{shape.border.style.solid}" },
"focusWidth": { "$value": "{shape.border.width.thick}" }
}
}Note the pairing rule hiding in focusWidth: focus emphasis is a width step, not a color invention—the focus ring is the same border machinery at thick (2px) in the focus color role. Components that invent a 3px focus border have forked the emphasis scale for everyone else.
Border Colors Are Semantic Roles
Border color lives in the semantic color layer with its own role ladder, resolved per mode like every color role:
// ui/designTokens/semantic/color.tokens.json (excerpt)
"border": {
"default": { light: neutral.300, dark: neutral.600 },
"subtle": { light: neutral.200, dark: neutral.700 },
"bold": { light: neutral.400, dark: neutral.500 }
}The inversion is the dark-mode lesson from the color foundation applied to lines: on light surfaces the default border is the lighter step (300) and on dark surfaces the darker-numbered neutral (600, which is lighter in value—naming follows the ramp, visibility follows the mode). Three roles cover the real range: subtle for internal divisions that must recede, default for component edges, bold for emphasis and active structure. Feedback borders (warning, error) come from the feedback color group—the same ramp-position discipline as their text and background siblings.
The 3:1 Non-Text Contrast Contract
When a border is the only visual signal of a boundary, state, or component edge, WCAG 1.4.11 requires 3:1 against the colors on both sides. Two practical consequences: a border.subtle role used to delineate an interactive control is probably a defect—subtle exists for divisions that carry no information; and every border-as-signal pair is a per-mode claim, the same as text contrast. The validator discipline from the color foundation extends directly: enumerate the pairs, check both modes, fail loudly.
Borders vs the Alternatives
- Border vs elevation: a shadow says "above"; a border says "edge." Cards that need separation but not height want a border or a surface step, not a whisper shadow.
- Border vs surface steps: dark modes favor surface-color steps over lines—the color system's
background.secondaryseparates without any stroke at all. - Border vs spacing: when a gap can separate as well as a line, prefer the gap—whitespace separates without adding visual weight.
System Roles: Where Border Decisions Land
Design Impact
Designers own the semantics of the line: which edges exist, which role each carries, and where whitespace replaces stroke entirely. The comp-review question is "which border role is this?"—an unnamed line is an unspecifiable one.
Engineering Impact
Engineers own consumption hygiene: widths and styles through the semantic control contract, colors through border roles, and the focus pairing (thick width + focus color) not forked per component.
Accessibility Impact
Accessibility owns the 3:1 ledger: the enumerated list of border-as-signal pairs, checked per mode, with subtle banned from carrying meaning alone. The ledger is boring and is the difference between auditable and hoped-for.
Design & Code Interplay
In the design tool, the border system appears as three stroke styles (the vocabulary) and the role ladder as named colors: designs specify lines by role, never by raw swatch. The two-width discipline reads as a library constraint—strokes snap to 1 or 2—and the dashed/dotted semantics appear in the component specs that use them (placeholder fields, drop zones) rather than as free stylistic choice.
The dark-mode comp obligation from elevation applies here too: bordered components are drawn in both modes, because border visibility inverts across the modes and the fix is a role remap, not a local tweak.
In code, the contract is three custom properties and the focus pairing:
/* Generated: app/designTokens.scss */
@layer core {
:root {
--core-shape-border-width-hairline: 1px;
--core-shape-border-width-thick: 2px;
}
}
/* Component consumption: role, width, style — all semantic */
[data-ds-component='TextField'] {
--ds-text-field-border-width:
var(--semantic-control-border-default-width, 1px);
--ds-text-field-border-color:
var(--semantic-color-border-default, #aeaeae);
}
.textField {
border: var(--ds-text-field-border-width)
solid
var(--ds-text-field-border-color);
}
/* Focus = the same machinery, one width step up, focus color */
.textField:focus-visible {
border-width: var(--semantic-control-border-focus-width, 2px);
border-color: var(--semantic-color-border-focus);
outline: none; /* the border IS the indicator */
}
/* Semantic styles carry meaning, not decoration */
.dropzone { border-style: var(--core-shape-border-style-dashed); }Applied Example: A Form Section, Framed Correctly
Frame a form section—two inputs and a drop zone—using the whole border stack:
- Region edge: the section groups related inputs, so it is separated from its siblings by whitespace (
spacing.size.07) and aborder.subtleinternal rule—division without emphasis. - Input edges: each input is interactive, so its boundary is signal:
border.defaultcolor atdefaultWidth(hairline)—which passes 3:1 in both modes per the ledger. - Focus: on focus, the same border steps to
focusWidth(thick, 2px) in the focus color—the emphasis scale, not a new style. Keyboard users see the same indicator as pointer users' hover, one step stronger. - Drop zone: dashed hairline—the style says "provisional target"—in
border.default, with the drag-over state switching toborder.boldplus the feedback accent, still no new widths. - Per-mode proof: dark mode shows the same section with its inverted role resolution; the 3:1 ledger entries for input edges and drop zone are re-checked, not assumed.
One vocabulary, three semantics, zero new values. The form looks framed rather than boxed because every line that exists, exists for a stated reason.
Constraints & Trade-offs
- Two widths vs gradient of emphasis: a locked hairline/thick pair forces emphasis to be binary; systems that need finer gradation usually need the color role ladder (
subtle/default/bold), not more widths. - Subtle borders vs pure whitespace:
border.subtlecosts paint and visual texture where a spacing gap often separates as well; the system bias is whitespace-first, lines when structure must be explicit. - Border indicators vs outlines: using the border as the focus indicator couples structure to state (a bordered component changes size by 1px on focus unless widths are swapped, not added); outline-based indicators avoid that but need their own radius pairing. Both are legitimate—the system requires picking one per component class and staying consistent.
- 3:1 floor vs brand hairlines: brand palettes sometimes mandate a barely-visible edge; the contract wins where the border carries meaning, and the brand adjust where it is decoration.
Common Pitfalls & Failure Modes
1. Invented widths
/* Bad: A third width nobody approved */
.card { border: 1.5px solid #ddd; }
/* Good: The emphasis scale */
.card { border: var(--semantic-control-border-default-width) solid
var(--semantic-color-border-default); }2. Meaning carried by subtle borders
/* Bad — An interactive control delineated below the 3:1 floor */
.chip { border: 1px solid var(--semantic-color-border-subtle); }
/* Good: Signal borders use the default role or bolder */3. Style as decoration
/* Bad: Dashed because it looked nice */
.panel { border-style: dashed; }
/* Good: Dashed means something — provisional, droppable, placeholder */4. Light-mode-only border checks
The pair that passes 3:1 on white routinely fails on the dark surface it also ships on. The ledger is per-mode or it is not a ledger.
5. Border-color literals
/* Bad */
.divider { border-top: 1px solid #e5e5e5; }
/* Good */
.divider { border-top: var(--semantic-control-border-default-width)
solid var(--semantic-color-border-subtle); }Border System Health Metrics
Signal 1: Vocabulary exclusivity
- Healthy: every border resolves through the semantic width/style/color contracts; a grep for
border:literals in product CSS returns only fallbacks. - Warning: a 1.5px survivor—the "between" width—circulates in one area, negotiating against the two-width discipline.
- Critical: arbitrary widths and hex borders are common; emphasis no longer has a scale and mode resolution is luck.
Signal 2: Contrast ledger completeness
- Healthy: every border-as-signal pair appears in the 3:1 ledger, checked per mode;
subtleappears only for divisions. - Warning: the ledger covers light mode; dark-mode entries are "assumed from the role mapping"—assumed is the warning word.
- Critical: a signal border below 3:1 in any mode—an invisible boundary shipping as a visible one.
Signal 3: Style semantics
- Healthy: dashed appears exactly where things are provisional (drop zones, placeholders); dotted where abbreviated.
- Warning: one decorative dashed panel—style drifting from meaning, and the next contributor cannot read the convention from the code.
- Critical: styles are aesthetic choices; the semantics are gone and the vocabulary no longer says anything.
Migration Strategy: Collecting the Lines
- Inventory the borders: every width, style, and color literal with its surface. Dedupe; most products find dozens of literal lines serving three roles.
- Snap to the vocabulary: widths to hairline/thick, styles to their semantics, colors to the role ladder—nearest-tolerance snapping with literals surviving as scoped fallbacks.
- Reclassify the boundaries: each line answers "edge, division, or decoration?"—whitespace replaces decoration,
subtletakes divisions,default+ takes edges that carry meaning. - Complete the ledger: every signal-bearing border enters the per-mode 3:1 check; the audit that follows is the point.
Real-World Case Studies
Case 1: The invisible input in dark mode
A tinted input edge cleared 3:1 on white and vanished on the dark surface—users tapped guesswork. The ledger entry (input edge, both modes) failed exactly where assumption had lived; the role remap to default restored legibility with one reference.
Case 2: The 1.5px compromise
A designer wanted "bolder than 1, subtler than 2" and shipped 1.5px on one component. Within a month three more surfaces negotiated their own widths. The rollback to the emphasis scale—thick width, bold color role—restored the binary that review could actually police.
Case 3: The dashed everything
A prototype's drop-zone styling leaked into production panels because it "looked technical." The style-semantics audit traced dashed to its three legitimate homes and reverted the rest—the vocabulary saying what it means again.
Verification Checklist
Additional Resources
- Color Foundations — the role ladder and per-mode resolution border colors live in (
/blueprints/foundations/color) - Radius & Shape — the corners these strokes wrap (
/blueprints/foundations/radius) - WCAG 1.4.11 non-text contrast — the 3:1 contract for meaningful borders (
https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html) - The sources —
ui/designTokens/core/shape.tokens.json,ui/designTokens/semantic/shape.tokens.json,ui/designTokens/semantic/color.tokens.json
Related Concepts
Reflection Questions
Apply This Concept
Write the 3:1 ledger for a search interface: list every border that carries meaning, its adjacent colors per mode, and which role each should use. Which entries are at risk?
Reflect
A teammate replaced a focus border with a box-shadow "because the 1px jump looked janky." What contract did the shadow break, and what are the two system-legal repairs?
