Divider
A hairline that separates — and the far more common case where spacing already did the job and a line is just noise.
Also called Separator, Rule, HR — in this system all of them are Divider.
Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.
Try spacing first
The same two groups, separated by a line and by air. The gap groups just as clearly and adds nothing to look at.
Vertical dividers
Between metadata on one line and between groups in a toolbar. Always shorter than the row, so they separate rather than enclose.
Labelled dividers
An "or" between two alternative paths. It is the one case where the divider is genuinely carrying meaning rather than just separating.
Inset dividers in a list
In a list with leading avatars, the divider starts at the text rather than the container edge — it separates the rows, not the whole panel.
Interaction states
Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.
Footer
Every part, every measurement, and the reason it is that number.
Content above
Content below
A one-pixel rule with more space above than below, and an optional label that interrupts the line rather than sitting on it.
- Thickness1px, never more
A 2px divider is a border, and a border implies a container. One pixel is enough at every density — the contrast does the work, not the weight.
- Colour--ds-border-subtle
The lightest border token. A divider drawn in the default border colour competes with the edges of the components around it.
- Margin above20px
More than below. The eye reads a rule as belonging to the content beneath it, so equal margins make it float between two blocks belonging to neither.
- Margin below16px
The asymmetry is the whole rule. Roughly a 5:4 ratio is enough to feel deliberate without looking like a mistake.
- Label12px, muted, centred
The line breaks around the label rather than running behind it. A label sitting on top of an unbroken rule reads as struck through.
- InsetAligned to the content
In a list with leading avatars the rule starts at the text, so it separates the rows rather than cutting the whole panel in half.
- Vertical heightShorter than its row
16–20px in a 44px toolbar. A full-height vertical rule reads as a container edge and the toolbar looks like two panels.
Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.
Color
| Token | Value | Used for |
|---|---|---|
| --ds-border-subtle | — | The rule itself |
| --ds-fg-muted | — | The optional label |
Spacing
| Token | Value | Used for |
|---|---|---|
| --space-5 | Margin above | |
| --space-4 | Margin below — deliberately less | |
| --space-2 | Vertical divider inline margins | |
| label gap | Break in the rule either side of a label |
Radius
| Token | Value | Used for |
|---|---|---|
| thickness | Every divider, at every density — 2px is a border |
Typography
| Token | Value | Used for |
|---|---|---|
| --text-caption | Label |
Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.
| Size | Height | Label gap | When to use |
|---|---|---|---|
| Horizontal | 1px | 20px above, 16px below | Between sections of equal weight. The asymmetry is not optional. |
| Vertical | 16–20px | 8px inline | In a toolbar or a metadata row. Always shorter than the row it sits in. |
| Inset | — | Aligned to content | In a list with leading content, starting at the text rather than the container edge. |
| Labelled | 1px | 12px either side of the label | The line breaks around the label, never runs behind it. |
| Card footer | — | 0 — the padding provides it | Full-bleed to the card edge, with the footer’s own padding doing the spacing. |
16px below
<hr aria-hidden="true" />
role="separator" only when it groupsSecurity
Card
Not a checklist to run at the end. These are the requirements the component was built from.
Contrast
- A decorative divider has no contrast requirement — it is aria-hidden and carries nothing.
- A divider that is the only grouping signal is meaningful non-text content and owes 3:1 under 1.4.11.
- In forced-colors mode, a divider drawn as a background disappears. Use a border or set forced-color-adjust so the boundary survives.
- A labelled divider’s text is content and owes 4.5:1.
Keyboard
| — | A divider is never focusable and never interactive. |
| Tab | Passes straight over it. A separator with a tabindex is always a bug. |
Screen readers
- A decorative divider should be silent. Twelve "separator" announcements down a settings page is noise that obscures the content.
- Where a divider genuinely groups, the group should also carry a label — role="group" with aria-label conveys far more than a bare separator.
- A labelled divider announces its label. "or" between two sign-in options is meaningful and should be heard.
Focus & touch
- A divider is never in the focus order. If it needs to be — as a resize handle, for instance — it is a different component with role="separator", tabindex and aria-valuenow, not a divider with extra behaviour.
- Dividers are not targets and need no touch consideration of their own — but they do compete for vertical space. On a phone, prefer spacing: a settings page with eight rules is eight pixels of ink and eight interruptions in a column that is already narrow.
| Attribute | Applied to | Notes |
|---|---|---|
| aria-hidden="true" | A decorative divider | The default. Most dividers convey nothing that spacing does not already convey. |
| role="separator" | A grouping divider | Only where the line is the sole grouping signal — between menu groups, for example. |
| aria-orientation="vertical" | A vertical separator | Horizontal is the default and does not need stating. |
| <hr> | A thematic break in prose | The native element already has role="separator". Reach for it before a styled div. |
Example usage
1import { Divider } from '@/ui/Display'23// Decorative by default: aria-hidden, no role.4<Divider />56// Vertical, in a toolbar. Always shorter than the row.7<Divider orientation="vertical" className="h-5" />89// Labelled — the one case where the divider carries meaning.10<Divider label="or" />1112// Meaningful: between groups in a menu, where the line is the ONLY grouping13// signal a sighted user gets.14<div role="menu">15 <button role="menuitem">Rename</button>16 <div role="separator" />17 <button role="menuitem">Delete</button>18</div>1920// The asymmetry, as a token pair. The eye reads a rule as belonging to the21// content BENEATH it.22.section-break {23 margin-block: var(--space-5) var(--space-4); /* 20px / 16px */24}2526// Before reaching for this component at all, try:27<Stack gap="lg">…</Stack>Framework-free HTML
<!-- Decorative. Silent to assistive tech. -->
<hr aria-hidden="true" class="ds-divider" />
<!-- Thematic break in prose. <hr> already has role="separator". -->
<p>The rollback completed in eight seconds.</p>
<hr />
<p>A postmortem was filed the following morning.</p>
<!-- Meaningful: the line is the only grouping signal here. -->
<div role="menu" aria-label="Actions">
<button type="button" role="menuitem">Rename</button>
<button type="button" role="menuitem">Duplicate</button>
<div role="separator"></div>
<button type="button" role="menuitem">Delete</button>
</div>
<!-- Vertical, in a toolbar. -->
<div role="toolbar" aria-label="Formatting">
<button type="button">…</button>
<span role="separator" aria-orientation="vertical"></span>
<button type="button">…</button>
</div>
<!-- Labelled: the rule breaks around the word rather than running behind it. -->
<div class="ds-divider ds-divider--labelled">
<span>or</span>
</div>CSS
.ds-divider {
border: 0;
block-size: 1px; /* never 2: that is a border */
background: var(--ds-border-subtle);
/* More above than below. The eye reads a rule as belonging to the content
beneath it, so equal margins leave it belonging to neither. */
margin-block: var(--space-5) var(--space-4);
}
.ds-divider[aria-orientation='vertical'],
.ds-divider--vertical {
inline-size: 1px;
/* Shorter than the row: a full-height rule reads as a container edge. */
block-size: 20px;
margin-block: 0;
margin-inline: var(--space-2);
}
/* The line breaks AROUND the label. A label on an unbroken rule reads as
struck through. */
.ds-divider--labelled {
display: flex;
align-items: center;
gap: 12px;
background: none;
block-size: auto;
color: var(--ds-fg-muted);
font-size: 12px;
}
.ds-divider--labelled::before,
.ds-divider--labelled::after {
content: '';
flex: 1;
block-size: 1px;
background: var(--ds-border-subtle);
}
/* In a list with leading avatars, separate the ROWS, not the panel. */
.ds-list > * + *::before {
content: '';
display: block;
block-size: 1px;
margin-inline-start: 52px; /* aligned to the text */
background: var(--ds-border-subtle);
}
/* A background-drawn rule vanishes in forced colors. */
@media (forced-colors: active) {
.ds-divider { border-block-start: 1px solid; block-size: 0; }
}Component API
Divider
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | 'horizontal' | 'vertical' | 'horizontal' | Vertical dividers are always shorter than the row they sit in. |
| label | ReactNode | — | Breaks the rule around a centred label. The one case where a divider carries meaning. |
| decorative | boolean | true | aria-hidden by default. Set false only where the line is the sole grouping signal. |
| className | string | — | Where the inset and the vertical height are applied — both are context, not variants. |
Professional tips
- Count the dividers on a screen. More than three or four usually means the layout is relying on lines where it should be relying on space.
- In a list, put the divider on the item rather than between items — a first-child or last-child rule is easier to reason about than a separate element per gap.
- Full-bleed dividers in a card should extend to the card edge, ignoring its padding. A rule that stops short of the border looks like a rendering error.
- For metadata rows, a vertical divider reads more cleanly than a bullet, which users mistake for list content.
- Do not animate a divider. It is a boundary, not an event, and a line that fades in draws attention it does not deserve.
Performance
- Prefer a border or a pseudo-element over an extra DOM node in long lists. A thousand-row list with a divider element per row is a thousand nodes for a thousand pixels.
- Use a single background-image with a linear-gradient for repeated dividers in a virtualised list rather than an element per gap.
- A 1px background can render at 0.5px or 1.5px on fractional device pixel ratios. Use a border where crispness matters, or accept the softness — it is usually invisible.
Common mistakes
- Reaching for a divider when the real fix was more spacing.
- Equal margins above and below, so the rule belongs to neither block.
- A rule under every heading, duplicating a boundary the heading already made.
- Full-height vertical dividers, which read as container edges.
- role="separator" on decorative dividers, producing a dozen pointless announcements.
- A 2px divider, which is a border and implies a container.
- Background-drawn rules that vanish in forced-colors mode.
- A divider between items that already have their own borders.
Real-world recommendations
- Dividers proliferate during design reviews because they are the easiest thing to ask for when something "feels cramped". Audit them a week later and most can go.
- In dense data UIs — tables, logs, terminals — dividers earn their place, because the spacing budget genuinely does not exist.
- Menus are the strongest case: separators between groups measurably reduce mis-selection, and they are one of the few places the line carries real information.
- If a page looks better with the dividers removed, remove them. That test takes ten seconds and is right more often than not.