Divider
A hairline rule that separates content. Horizontal or vertical, solid / dashed / dotted, three thickness steps, tonal strength that adapts to coloured surfaces, an inline label, and an inset that aligns the line to the row's text edge.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { Divider } from "@tessinaui/ui";<Divider />When to use
Use a divider when whitespace alone cannot group content — a list of rows that would otherwise run together, a toolbar with two groups of icons, a total that must read as separate from the lines above it.
When not to use. Reach for space or a card first. Material, Carbon and Fluent all say the same thing, and the app survey agreed: nine of the surveyed products group content with a card and a gap and ship no rule at all. A divider between every item makes a list look busier, not clearer — divide groups, not peers.
A centred caption with no rule either side (a chat day-stamp, a settings
section caption) is not a divider. That is a Label or a Badge.
Examples
Default
A horizontal rule separating two blocks of content.
Inset
The list separator every phone ships: the rule starts where the row's text
starts, not at the frame, so a leading icon column reads as one stack. Set
--divider-inset to the row's own inline padding. On a vertical divider the
same prop shortens the rule along its own axis — the toolbar rule that stops
short of the bar's edges.
Variants
Three line styles — solid, dashed, and dotted. Dashed and dotted read as
receipts, tickets and pricing cards.
Sizes
Three thickness steps: md (1px hairline, the default), lg (2px) and xl
(4px). Anything heavier than 4px is a filled band rather than a line — paint it
with a Surface.
Tones
subtle, default and strong are steps of the same strength ladder. On a
coloured plate the ladder is re-expressed in the surface's own ink, and
Divider picks that up from the enclosing Surface on its own — you do not
pass tone="on-color" by hand.
Emphasis
Weight carries meaning: a hairline separates peers, a heavier and darker rule marks the line a total sits below.
With label
An inline label between two flanking rules — the "or" of an auth screen.
labelPosition moves it to the start or end.
Vertical
Vertical rules separating inline items. A vertical divider stretches to its flex parent, so that parent needs a height.
Semantic separator
Most dividers are decoration and stay silent. decorative={false} emits a real
role="separator"; give it a label and the name is wired up by reference so
it is actually announced.
Custom element
render swaps the host element — most usefully <li>, because a <div>
between list items breaks the list for assistive tech.
Variants
| Variant | Description |
|---|---|
solid | Continuous line. Default. |
dashed | Evenly spaced dashes. |
dotted | Fine dotted pattern. |
Sizes
| Size | Thickness | When to use |
|---|---|---|
md | 1px | The hairline. Default, and correct for almost every list. |
lg | 2px | Emphasis — the rule above a checkout total, a table header. |
xl | 4px | A hard section break. |
Heavier than 4px is a band, not a line: in every surveyed app, thick section
splits were a filled background showing between grouped blocks. Use a
Surface.
Orientations
| Orientation | Notes |
|---|---|
horizontal | Spans the full width of its container. Default. |
vertical | Stretches to a flex parent (align-self: stretch); that parent must have a height. |
Tones
| Tone | On a plain surface | On a coloured surface | When to use |
|---|---|---|---|
subtle | border/50 | current/10 | Barely-there separation inside grouped content. |
default | border | current/20 | Standard separation. Default. |
strong | muted-foreground | current/40 | Emphasis — section breaks, checkout totals. |
on-color | — | current/20 | Only needed when you paint a coloured plate yourself, without a Surface. |
The on-color column is a tint of the surface's own ink, never a literal
white: an on-color surface is not always dark (in dark mode --primary and
--foreground are near-white), and a white rule on one of those is invisible.
Inset
inset pulls the ends of the rule in along its own axis, so the line can start
at the row's text edge instead of the frame. This is the default separator
shape on iOS and Material's inset / middle divider, and it is what
CONVENTIONS §7b.1 asks for inside popups.
| Value | Effect |
|---|---|
none | Full-bleed, edge to edge. Default. |
start | Inset at the leading end only — the iOS list separator. |
end | Inset at the trailing end only. |
both | Inset at both ends — Material's "middle" divider. |
The distance is the --divider-inset custom property, default 1rem. Set it
to whatever your row actually uses:
<Divider inset="start" className="[--divider-inset:3.25rem]" />Insets use logical properties, so they follow dir without a second map.
Labels
Pass label (or children) to render text between two flanking rules.
labelPosition controls where the label sits.
<Divider label="or" />
<Divider label="Recently" labelPosition="start" />
<Divider label="Archive" labelPosition="end" />For a section caption above a rule — the form-section pattern — compose a heading and a plain divider instead. That keeps the heading a real heading in the accessibility tree, which a label inside a divider can never be.
<h3 className="text-sm font-medium">Billing</h3>
<Divider spacing="sm" />Accessibility
By default a Divider is decorative: it emits no role at all, both
rules are aria-hidden, and screen readers skip it. Set decorative={false}
when the boundary carries meaning, and it emits role="separator" with the
matching aria-orientation.
Emitting no role rather than role="none" is deliberate. On the default <div>
the two are equivalent — a <div> is already generic — but role="none"
overwrites whatever host render supplies, and a render={<li />} divider
carrying role="none" strips the list item's role and breaks the whole list.
Pass role="none" explicitly if you want it.
<Divider /> {/* silent */}
<Divider decorative={false} /> {/* semantic separator */}
<Divider decorative={false} orientation="vertical" /> {/* vertical separator */}A labelled semantic separator is a trap, and this component steps around it.
WAI-ARIA marks separator as children presentational: any text nested inside
one is erased for assistive tech, so a labelled divider that puts its text in
the separator element announces nothing at all. (Ant Design and Mantine both
ship exactly that bug.) separator does still take a name from the author, so
Divider gives the label an id and points aria-labelledby at it — the rule is
announced with its name. A consumer's own aria-label or aria-labelledby
always wins.
<Divider decorative={false} label="Archive" />
{/* announced as "Archive, separator" */}Inside a list, use render={<li />}. A <div> between <li>s is invalid list
structure and assistive tech will report the list length wrongly.
Contrast here is a non-text concern (WCAG 1.4.11). subtle is deliberately a
low-contrast tone: it is for decorative separation inside content that is
already grouped, and must never be the sole boundary of a control. Measured
ratios for every tone in both themes are in the component's variant ledger.
Assistive technology matrix
| AT | Status |
|---|---|
| VoiceOver (macOS) | Passed (2026-09-10, Safari) — a decorative divider is silent; decorative={false} announces "horizontal splitter"; with a label, "horizontal splitter Kestrel". Transcript in the variant ledger |
| NVDA | Untested — not runnable on the maintainers' machines |
| JAWS | Untested — not runnable on the maintainers' machines |
RTL
The divider uses logical properties throughout — border-inline-start for the
vertical rule, margin-inline for spacing and insets — so vertical rules and
start/end insets flip correctly in right-to-left layouts. Pass dir="rtl" when
you need explicit direction context.
<Divider label="أو" labelPosition="start" dir="rtl" />API Reference
Divider
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Layout axis |
variant | "solid" | "dashed" | "dotted" | "solid" | Line style |
size | "md" | "lg" | "xl" | "md" | Thickness: 1 / 2 / 4 px |
tone | "default" | "subtle" | "strong" | "on-color" | "default" | Strength of the rule; adapts to a coloured surface |
spacing | "none" | "xs" | "sm" | "md" | "lg" | "xl" | "none" | Gap opened across the rule |
inset | "none" | "start" | "end" | "both" | "none" | Pull the rule's ends in along its own axis |
label | ReactNode | — | Inline label between two flanking rules |
labelPosition | "start" | "center" | "end" | "center" | Where the label sits along the axis |
decorative | boolean | true | If false, emits role="separator" + aria-orientation |
render | RenderProp | — | Swap the host element (e.g. <li />) |
dir | "ltr" | "rtl" | — | Text direction |
role | AriaRole | — | Explicit role override |
className | string | — | Extra classes on the root element |
The component extends all standard div HTML attributes.
Exports
| Export | Type | Description |
|---|---|---|
Divider | component | The rule itself |
dividerVariants | cva | The line variants, for composing a rule elsewhere |
Data attributes
Stable hooks for styling, testing and automation.
| Attribute | On | Value |
|---|---|---|
data-slot="divider" | root | — |
data-slot="divider-line" | each rule | — |
data-slot="divider-label" | label | — |
data-orientation | root | horizontal | vertical |
data-variant | root | solid | dashed | dotted |
data-tone | root | default | subtle | strong | on-color |
data-on-color | root | "true" when on a coloured surface; absent otherwise |
data-inset | root | none | start | end | both |
CSS custom properties
| Property | Default | Effect |
|---|---|---|
--divider-inset | 1rem | Distance the inset prop pulls each end in |