Divider
Section separator with optional label and orientation.
Live demo
01Plain horizontal
Content above the divider.
Content below the divider.
With centred label
Left-aligned section header
- Alex Morgan β 2 min ago
- Jamie Chen β 14 min ago
- Sam O'Brien β 3 days ago
- Riley Fox β 6 days ago
Thicknesses & line styles
thinmedium dashed
thick dotted
brand colour
Vertical between toolbar groups
Implementation
02<script>
import Divider from '$lib/components/Divider.svelte';
</script>
β
<Divider />
<Divider label="OR" />
<Divider label="Recently active" labelPosition="left" />
<Divider thickness="thick" lineStyle="dashed" colour="#146ef5" />
<Divider orientation="vertical" />Divider is a structural separator. With no label it renders a native <hr> for free role="separator". With a label it becomes a div[role="separator"] flanked by two lines, and switches to a vertical bar with aria-orientation when orientation="vertical".
Logic explainer
03What Does It Do? (Plain English)
Divider draws a visual separator between sections of UI. Plain horizontal? It renders a native <hr>. With a label like "OR" between two login buttons, or as a vertical pipe between toolbar groups, it switches to a <div role="separator"> with two flanking lines around the label. Three thicknesses, three line styles, any colour you want, and proper ARIA either way.
Think of it as a piece of punctuation for layouts β the comma between form sections, the colon between toolbar groups, the "OR" between two equally good choices.
How It Works (Pseudo-Code)
state:
orientation β { 'horizontal', 'vertical' }
thickness β { 'thin', 'medium', 'thick' } // 1 / 2 / 4 px
lineStyle β { 'solid', 'dashed', 'dotted' }
label = '' or any string
labelPosition β { 'left', 'center', 'right' }
colour = optional CSS colour string
decorative = boolean β when true, hide from AT entirely
children = optional snippet that overrides label
derive:
hasLabel = Boolean(label || children)
customStyle = colour ? `--divider-colour: ${colour};` : ''
render:
if orientation === 'horizontal' AND not hasLabel:
<hr class="divider divider-h divider-{thickness} divider-{lineStyle}">
// Browsers expose role="separator" automatically β free ARIA.
else if orientation === 'horizontal' AND hasLabel:
<div role="separator" class="divider-with-label divider-label-{labelPosition}">
<span class="divider-line ... divider-line-start"></span>
<span class="divider-label">{children ?? label}</span>
<span class="divider-line ... divider-line-end"></span>
</div>
else (vertical):
<div role="separator" aria-orientation="vertical"
class="divider divider-v divider-{thickness} divider-{lineStyle}">
if decorative:
add aria-hidden="true" to the rendered element AND drop role="separator".There is no $effect, no observer, no JS-side state at all β Divider is a pure render function plus a couple of derived values. The "logic" is choosing between three render paths.
The Core Concept: Three Render Paths, One Component
Most divider components reach for <hr> and try to bolt a label on with ::before / ::after pseudo-elements. That works visually but the result is a mess of overlapping text-on-a-line that screen readers find confusing. Divider sidesteps this by branching on whether there's a label.
input output
βββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ
horizontal, no label <hr role="separator" (implicit)>
horizontal, label <div role="separator">
<span line/><span label/><span line/>
</div>
vertical <div role="separator"
aria-orientation="vertical">
decorative=true aria-hidden="true"; role removedThree concrete benefits:
- Semantic correctness for free. The
<hr>path uses the browser's built-inrole="separator". No extra ARIA, no fight with assistive tech. - Real label rendering. When a label is present the spans use flexbox (
flex: 1on each line) rather than absolutely-positioned pseudo-elements. The label can be any text length, can wrap, can be a snippet (e.g. an icon), and the lines on either side reflow correctly. - Decorative escape hatch. When the divider is purely visual β say, a dashed flourish under a heading β
decorative={true}strips the separator role and addsaria-hidden, so AT skips it. The visual output is identical; the ARIA tree is cleaner.
Label-position trick
.divider-label-left .divider-line-start { flex: 0 0 1.5rem; }
.divider-label-right .divider-line-end { flex: 0 0 1.5rem; }The default has both flanking lines at flex: 1, so the label sits dead-centre. To shove the label left, the left line is pinned to a fixed 1.5 rem and the right line keeps flex: 1 β it absorbs the rest of the width. Mirror image for right. Two lines of CSS replace what otherwise needs a Grid template.
Colour Theming via CSS Custom Property
Every line β <hr>, the two flanking spans, the vertical bar β reads its colour from a single custom property:
:global(:root) { --divider-colour: #e2e8f0; }
hr.divider-thin { border-top: 1px solid var(--divider-colour); }
.divider-line.divider-thin { border-top: 1px solid var(--divider-colour); }
.divider-v.divider-thin { border-left: 1px solid var(--divider-colour); }When a caller passes colour="#146ef5", the component writes style="--divider-colour: #146ef5;" on the wrapper. The custom property cascades to every internal element automatically β no per-element style override, no conditional class.
That cascading also means a parent can set --divider-colour on a section and every Divider inside picks it up without any prop drilling β a useful pattern for dark-mode themes.
State Flow Diagram
ββββββββββββββββββββββββββββββββββββββ
β props in β
β - orientation β
β - thickness, lineStyle β
β - label / children β
β - colour, decorative β
βββββββββββββββββββ¬βββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β hasLabel? β
ββββββββββ¬ββββββββββ
β
βββββββββββββββ΄ββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββ ββββββββββββββββββββββββ
β horizontal + β β horizontal + label β
β no label? β β β <div role= β
β β <hr> β β "separator"> β
β (free ARIA) β β + 3 spans β
ββββββββββββββββββββββ ββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββ
β vertical? β
β β <div β
β role= β
β "separator" β
β aria- β
β orientation= β
β "vertical"> β
ββββββββββββββββββββ
decorative=true βββΆ role removed, aria-hidden=true added
(applies to whichever branch was taken)Props Reference
| Prop | Type | Default | Description |
|---|---|---|---|
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Direction of the separator. |
thickness |
'thin' | 'medium' | 'thick' |
'thin' |
Line weight (1 / 2 / 4 px). |
lineStyle |
'solid' | 'dashed' | 'dotted' |
'solid' |
CSS border-style for the line. |
label |
string |
'' |
Visible label text. Omit for a plain line. |
labelPosition |
'left' | 'center' | 'right' |
'center' |
Where the label sits along the line. |
colour |
string |
'#e2e8f0' |
Any CSS colour value. Sets --divider-colour on the wrapper. |
decorative |
boolean |
false |
When true, hides from assistive tech (aria-hidden, no separator role). |
class |
string |
'' |
Extra classes on the wrapper. |
children |
Snippet |
β | Custom label content (overrides label). Useful for icon labels. |
Edge Cases
| Situation | Behaviour |
|---|---|
label and children both provided |
children wins β the snippet renders, the string is ignored. |
decorative={true} and label present |
The separator role is dropped and aria-hidden="true" is set. Screen readers skip it entirely; the visual output is unchanged. |
colour set on a parent via style="--divider-colour: red" |
The Divider inherits the colour without any prop being passed. Useful for theming sections. |
orientation="vertical" inside a non-flex container |
The bar has min-height: 1rem so it shows up even with no parent height; otherwise it would collapse to zero. Use a flex parent for full-height bars. |
labelPosition="left" with a long label |
The left flanking line stays at 1.5 rem; the right line expands to fill. The label can wrap if the container narrows enough. |
thickness="thick" with lineStyle="dotted" |
Renders as 4 px dotted β browsers handle the dot spacing automatically. |
Used inside a flex row without align-self: stretch on the vertical variant |
The bar has align-self: stretch baked in; it'll always match the row height. |
Dependencies
- Svelte 5.x β
$propsand$derivedfor the small amount of conditional rendering. No effects, no lifecycle. - Zero external dependencies β pure CSS, no JS animation, no icon library.
File Structure
src/lib/components/Divider.svelte # implementation
src/lib/components/Divider.md # this file
src/lib/components/Divider.test.ts # vitest unit tests
src/routes/divider/+page.svelte # demo pageAPI
04| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Direction of the line. |
thickness | 'thin' | 'medium' | 'thick' | 'thin' | Line weight (1 px / 2 px / 4 px). |
lineStyle | 'solid' | 'dashed' | 'dotted' | 'solid' | Border line style. |
label | string | undefined | Optional label rendered between two flanking lines. |
labelPosition | 'left' | 'center' | 'right' | 'center' | Where the label sits along the divider. |
colour | string | theme | Override the line colour with any CSS colour value. |
decorative | boolean | false | Hide from assistive tech with role="presentation". |