DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
SurfaceContainerStackFlexGridSpacerDividerAspectRatioScroll Area
AI agentsContributing
  1. Components
  2. Divider

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.

$ pnpm add @tessinaui/ui

When to use

  • Whitespace alone cannot group the content. A list of rows that would otherwise run together, or a total that must read as separate from the lines above it.
  • Two groups of controls share a bar. A toolbar with two groups of icons gets a vertical rule between them.
  • List rows with a leading icon or avatar. inset starts the rule at the row's text edge, so the icon column reads as one stack.
  • An "or" between two choices. label puts a short word between two flanking rules, the way an auth screen does.
  • Groups, not peers. Divide the groups; leave items inside a group to the gap.

When not to use

  • When space or a card already groups the content. Reach for gap and padding first. The app survey agreed: nine of the surveyed products group content with a card and a gap and ship no rule at all.
  • Between every item. A divider between every item makes a list look busier, not clearer.
  • Inside a card or modal that already separates its content. The container is the boundary.
  • As a section caption. A centred caption with no rule either side (a chat day-stamp, a settings section caption) is not a divider. Use Label or Badge; for a heading above a rule, compose a heading and a plain divider (see Labels).
  • For a thick band. Heavier than xl is a filled background; paint it with a Surface.
  • As decoration, or to cover a layout problem. A rule should clarify grouping; fix the spacing instead of hiding it behind a line. Gaps are Spacer and Stack work.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Divider } from "@tessinaui/ui";
example.tsx
<Divider />

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.

On a coloured plate

Every way a divider meets colour: a paired Surface background, hand-painted dark and pale plates (on a pale plate the ink is black, so the rule darkens), a vertical rule, and a plate painted without a Surface, where you pass tone="on-color" yourself. A label on a plate takes the plate's own muted ink rather than a grey, which would sink below AA on a saturated fill.

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.

API Reference

Divider

PropTypeDefaultDescription
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
labelReactNode—Inline label between two flanking rules
labelPosition"start" | "center" | "end""center"Where the label sits along the axis
decorativebooleantrueIf false, emits role="separator" + aria-orientation
renderRenderProp—Swap the host element (e.g. <li />)
dir"ltr" | "rtl"—Text direction
roleAriaRole—Explicit role override
classNamestring—Extra classes on the root element

The component extends all standard div HTML attributes.

Exports

ExportTypeDescription
DividercomponentThe rule itself
dividerVariantscvaThe line variants, for composing a rule elsewhere

Data attributes

Stable hooks for styling, testing and automation.

AttributeOnValue
data-slot="divider"root—
data-slot="divider-line"each rule—
data-slot="divider-label"label—
data-orientationroothorizontal | vertical
data-variantrootsolid | dashed | dotted
data-tonerootdefault | subtle | strong | on-color
data-on-colorroot"true" when on a coloured surface; absent otherwise
data-insetrootnone | start | end | both

CSS custom properties

PropertyDefaultEffect
--divider-inset1remDistance the inset prop pulls each end in

Variants

VariantDescription
solidContinuous line. Default.
dashedEvenly spaced dashes.
dottedFine dotted pattern.

Sizes

SizeThicknessWhen to use
md1pxThe hairline. Default, and correct for almost every list.
lg2pxEmphasis — the rule above a checkout total, a table header.
xl4pxA 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

OrientationNotes
horizontalSpans the full width of its container. Default.
verticalStretches to a flex parent (align-self: stretch); that parent must have a height.

Tones

ToneOn a plain surfaceOn a coloured surfaceWhen to use
subtleborder/50current/10Barely-there separation inside grouped content.
defaultbordercurrent/20Standard separation. Default.
strongmuted-foregroundcurrent/40Emphasis — section breaks, checkout totals.
on-color—current/20Only 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 the common inset / middle divider, and it is what CONVENTIONS §7b.1 asks for inside popups.

ValueEffect
noneFull-bleed, edge to edge. Default.
startInset at the leading end only — the iOS list separator.
endInset at the trailing end only.
bothInset at both ends — the "middle" divider.

The distance is the --divider-inset custom property, default 1rem. Set it to whatever your row actually uses:

example.tsx
<Divider inset="start" className="[--divider-inset:3.25rem]" />

Insets use logical properties, so they follow dir without a second map. They are padding on the divider's own box, so the rule stops exactly at the inset and never pushes past its container — tune --divider-inset rather than adding px-*, which would replace it.

Labels

Pass label (or children) to render text between two flanking rules. labelPosition controls where the label sits.

example.tsx
<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.

example.tsx
<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. A labelled decorative divider is the exception: the rules are still skipped, but the label is read as ordinary text — the "or" on an auth screen means something. Set decorative={false} when the boundary itself 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.

example.tsx
<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. 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.

example.tsx
<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

ATStatus
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
NVDAUntested — not runnable on the maintainers' machines
JAWSUntested — not runnable on the maintainers' machines

RTL

The divider uses logical properties throughout — border-inline-start for the vertical rule, margin-inline for spacing, padding-inline for insets — so vertical rules and start/end insets flip correctly in right-to-left layouts. labelPosition="start" puts the label on the right under dir="rtl". Pass dir when you need explicit direction context.

SpacerAspectRatio

On this page

When to useWhen not to useInstallationUsageExamplesDefaultInsetVariantsSizesTonesOn a coloured plateEmphasisWith labelVerticalSemantic separatorCustom elementAPI ReferenceDividerExportsData attributesCSS custom propertiesVariantsSizesOrientationsTonesInsetLabelsAccessibilityA labelled semantic separator is a trap, and this component steps around it.Assistive technology matrixRTL