jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · Layout

separator — <hr> is the separator

The W3C already built this one: <hr> carries thematic-break semantics, announcements, and styling for free. Only the vertical posture — splitting inline peers — has no native element, so it takes the ARIA route: a div with role=separator. The ink paints no color: the default variant, fused, is the backdrop's own contrast ghost, every shaped variant rides the same engine, and solid is the one plain-fill escape.

install — separator

$npx jixoai-ui add separator

https://ui.jixoai.com/r/separator.json

native <hr> role=separator vertical contrast ghost ink 7 variants · ghost + mask + blend

overview

Overview

Two postures, one ink law, zero color: the platform carries the semantics, the family paints physics, and the eight axes ride along unread — the fleet's zero-reader class, measured.

The postures are the platform's. Horizontal renders the native <hr> — thematic-break semantics and the separator announcement for free, zero ARIA authored; only the vertical posture has no native element, so it takes the WAI-ARIA route: a div with role=separator and aria-orientation the component owns (landed AFTER the attribute spread — not overridable). Length stays your layout's job on purpose: block-stretch horizontal, inline-peer vertical, no length API.

The ink paints no color (Owner ruling, 2026-09-01). The default variant is NAMED fused — the backdrop's own CONTRAST GHOST, a backdrop-filter: contrast(0.5) strip that reads as a tonal shift over any ground; dashed (6/4), dense (3/3), dotted and wavy are MASKS over that same strip; fade rides the BLEND engine — an alpha-ramped white gradient under mix-blend-mode: difference, peak α capped at 0.6 (the exact-mid blind spot is difference's, and documented, not fixable); solid is the ONE additive exception — ghost off, plain var(--border) (Owner amendment, 2026-09-08). Six of seven variants therefore read NO token at all — theme-free by mechanism, adaptive over any ground in any scope. Solid is the family's only token read, and its alias is ROOT-PINNED: --jx-border: var(--border) resolves at the root, so the light ink oklch(0 0 0) propagates pre-substituted — a scoped .dark island cannot re-derive it (measured frozen), and at root-level dark the UNPINNED solid FLIPS oklch(0 0 0) → oklch(1 0 0) (measured in place and by a body-level move — the earlier "frozen everywhere" holds were the theming-demo stage's data-theme="light" pin). THREE TIERS, all measured at the paint: root dark flips (live :root substitution), mid-tree .dark islands cannot, data-theme stages locally pin.

The eight axes are the fleet's ZERO-READER CLASS (the stack structural class, measured): density is a managed stamp, size stamps --jx-size-effective with nothing behind it reading, and shape / radius / color / elevation / motion supply unread (grep receipts over ui/separator/) — the supply chain to composed descendants is the point, not consumption. Kinship: stack (gaps instead of rules — the layout alternative), menu and button-group (the strip living inside grouped chrome), timeline (the ruled scale).

separator

Horizontal renders the native hr; vertical renders the ARIA div and stretches its container's cross axis.

A paragraph of ordinary copy above the rule.


first second third

And copy below it — the thematic break reads natively.

separator-usage.svelte svelte
<Separator />                        <!-- hr: the contrast ghost -->
<Separator orientation="vertical" />  <!-- ARIA div: inline peer split -->
<Separator variant="dashed" />        <!-- 6/4 dashes over the ghost -->
<Separator variant="wavy" />          <!-- the SVG sine mask -->
<Separator variant="fade" />          <!-- blend: transparent→dark→transparent -->
<Separator variant="solid" />         <!-- the --border plain-fill escape -->

<!-- length is layout: -->
<Separator class="my-6" />
<div class="h-5"><Separator orientation="vertical" /></div>

ink engine

The ink engine — no color, only physics

A separator paints no color (Owner ruling, 2026-09-01): border-color is for borders. The default variant is NAMED fused — the backdrop's own CONTRAST GHOST, a backdrop-filter: contrast(0.5) strip that reads as a tonal shift over any ground. Dashed, dense, dotted and wavy are MASKS over that same strip; fade rides the BLEND engine — an alpha-ramped white gradient under mix-blend-mode: difference, inverting the backdrop toward mid exactly as its alpha ramps: transparent → light → dark → light → transparent. The one exception: solid (Owner amendment, 2026-09-08) turns the ghost off and paints plain var(--border) — the escape for grounds where the ghost's exact-mid blind spot or a patterned backdrop defeats subtraction.

separator · ink engine

fused
the contrast ghost (default)
solid
the plain-fill escape — var(--border), ghost off
dashed
6/4 dashes
dense
3/3 dense dashes
dotted
a chain of dots
wavy
the SVG sine mask
fade
blend: transparent → dark → transparent
auto-adaptive proof — over a light→dark→light gradient


the ghost and its masks track the whole ramp; the fade's blend eases toward exact mid-gray — its one blind spot — and stays a tonal shift everywhere else. No color token anywhere.
separator-ink-demo.svelte svelte
<script lang="ts">
  import Separator from '@ui/separator.svelte';
</script>

<div class="flex w-full max-w-lg flex-col gap-4">
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">fused</span>
    <Separator />
    <span class="text-muted-foreground text-[12px]">the contrast ghost (default)</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">solid</span>
    <Separator variant="solid" />
    <span class="text-muted-foreground text-[12px]">the plain-fill escape — var(--border), ghost off</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">dashed</span>
    <Separator variant="dashed" />
    <span class="text-muted-foreground text-[12px]">6/4 dashes</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">dense</span>
    <Separator variant="dense" />
    <span class="text-muted-foreground text-[12px]">3/3 dense dashes</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">dotted</span>
    <Separator variant="dotted" />
    <span class="text-muted-foreground text-[12px]">a chain of dots</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">wavy</span>
    <Separator variant="wavy" />
    <span class="text-muted-foreground text-[12px]">the SVG sine mask</span>
  </div>
  <div class="flex flex-col gap-1.5">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">fade</span>
    <Separator variant="fade" />
    <span class="text-muted-foreground text-[12px]">blend: transparent → dark → transparent</span>
  </div>
</div>

<!-- the auto-adaptive proof: the ghost and its masks track the whole
     light→dark→light ramp; no color token anywhere -->
<div
  class="flex w-full max-w-lg flex-col gap-4 border border-border p-4"
  style="background: linear-gradient(90deg, oklch(0.98 0 0), oklch(0.35 0 0), oklch(0.98 0 0))"
>
  <Separator />
  <Separator variant="dashed" />
  <Separator variant="fade" />
</div>

demo

Length is layout

The component draws the line; the consumer decides how long it is. Horizontal rules stretch to their container (or any width class); vertical rules stretch the container's cross axis — put one in a fixed-height flex row and it fills it.

separator · length

full width — the default stretch
class=x3hqpx7 — any width class
class=x1gqa6jt — length is also rhythm
h-8 row the rule fills the cross axis
separator-length-demo.svelte svelte
<script lang="ts">
  import Separator from '@ui/separator.svelte';
</script>

<div class="flex w-full max-w-md flex-col gap-5">
  <div class="flex flex-col">
    <span class="text-muted-foreground text-[11px]">full width — the default stretch</span>
    <Separator />
  </div>
  <div class="flex flex-col">
    <span class="text-muted-foreground text-[11px]">class="w-1/2" — any width class</span>
    <Separator class="w-1/2" />
  </div>
  <div class="flex flex-col">
    <span class="text-muted-foreground text-[11px]">class="my-6" — length is also rhythm</span>
    <Separator class="my-6" />
  </div>
  <div class="flex h-8 items-stretch gap-4 text-[13px]">
    <span>h-8 row</span>
    <Separator orientation="vertical" />
    <span>the rule fills the cross axis</span>
  </div>
</div>

W3C foundation

What the platform gives

No ARIA to maintain on the horizontal path — the browser announces <hr> as a separator to assistive tech. The vertical path is the WAI-ARIA separator pattern, aria-orientation included.

usage
<Separator />                        <!-- hr: the contrast ghost -->
<Separator orientation="vertical" />  <!-- ARIA div: inline peer split -->
<Separator variant="dashed" />        <!-- 6/4 dashes over the ghost -->
<Separator variant="wavy" />          <!-- the SVG sine mask -->
<Separator variant="fade" />          <!-- blend: transparent→dark→transparent -->
<Separator variant="solid" />         <!-- the --border plain-fill escape -->

<!-- length is layout: -->
<Separator class="my-6" />
<div class="h-5"><Separator orientation="vertical" /></div>

types

Types

Two postures: the native hr for thematic breaks, the ARIA div for inline peer splits.

separator · types

horizontal (default)
the native hr — thematic break between blocks
vertical
firstsecond
role=separator div — splits inline peers, stretches the cross axis
separator-types-demo.svelte svelte
<script lang="ts">
  import Separator from '@ui/separator.svelte';
</script>

<div class="flex flex-wrap items-start gap-6">
  <div class="flex min-w-56 flex-col gap-3 border border-border p-4">
    <span class="font-nav text-primary text-[11px] uppercase tracking-[0.24em]">horizontal (default)</span>
    <Separator />
    <span class="text-muted-foreground text-[12.5px]">the native hr — thematic break between blocks</span>
  </div>
  <div class="flex min-w-56 flex-col items-center gap-3 border border-border p-4">
    <span class="font-nav text-primary self-start text-[11px] uppercase tracking-[0.24em]">vertical</span>
    <div class="flex h-10 items-stretch gap-4 text-[13px]">
      <span>first</span>
      <Separator orientation="vertical" />
      <span>second</span>
    </div>
    <span class="text-muted-foreground self-start text-[12.5px]">role=separator div — splits inline peers, stretches the cross axis</span>
  </div>
</div>

usage

Usage

One prop, no length API on purpose — length is your layout's job.

Separator usage
<Separator />                        <!-- hr: the contrast ghost -->
<Separator orientation="vertical" />  <!-- ARIA div: inline peer split -->
<Separator variant="dashed" />        <!-- 6/4 dashes over the ghost -->
<Separator variant="wavy" />          <!-- the SVG sine mask -->
<Separator variant="fade" />          <!-- blend: transparent→dark→transparent -->
<Separator variant="solid" />         <!-- the --border plain-fill escape -->

<!-- length is layout: -->
<Separator class="my-6" />
<div class="h-5"><Separator orientation="vertical" /></div>

theming

Theming

No color decision of its own — the ink is physics, not palette: the contrast ghost adapts to whatever ground it crosses, the blend fade inverts it, and the one plain fill (solid) simply reads the --border token. Length comes from layout.

xs
ab
sm
ab
default
ab
lg
ab
TokenDefaultSource
contrast ghostbackdrop-filter: contrast(0.5)structural
masks (dashed/dense/dotted/wavy)mask over the ghost stripstructural
blend fademix-blend-mode: differencestructural
solid fillvar(--border)color

api

API

Props from the Separator Props interface — everything else rides through as native hr attributes.

Properties

PropertyTypeDefaultDescription
orientation 'horizontal' | 'vertical''horizontal'horizontal renders the native hr; vertical renders the role=separator div. The mask axis swaps with it.
variant 'fused' | 'solid' | 'dashed' | 'dense' | 'dotted' | 'wavy' | 'fade''fused' · Own default, not ambientThe ink geometry: fused is the bare contrast ghost (the named default); dashed (6/4), dense (3/3), dotted and wavy are masks over it; fade rides the blend engine; solid is the plain-fill var(--border) escape — the subtraction-ink exception (Owner 2026-09-08). Own default, not ambient (ink geometry is never a paint-zone rung).
class string''Class passthrough — width/height/margin live here, by design.
style string | null—Inline style passthrough, composed AFTER the family's carrier stamp (the #4 seam law: never clobbered, never dropped).
...rest HTMLAttributes<HTMLHRElement>spreadEvery other attribute lands on the element (vertical spreads onto the div).

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

axes

The eight axes on separator

The fleet's ZERO-READER CLASS, measured (the stack structural class): density is a managed stamp (data-density, nothing reads it), size stamps --jx-size-effective with no consumer behind it, and shape / radius / color / elevation / motion supply unread — grep receipts over ui/separator/. The one token read in the whole family is solid's var(--border) fill (the additive exception), and the theme split is BY MECHANISM, measured: six variants are physics (theme-free everywhere), one rides a :root alias — frozen under a scoped .dark island, FLIPPED by root-level dark when unpinned, locally PINNED by data-theme demo stages (three tiers). The supply chain to composed descendants is the point, not consumption.

PropertyTypeDefaultDescription
density '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number (+ the five legacy spellings)'auto'MANAGED STAMP, ZERO-READER CLASS (the stack structural class) — the resolved rung is stamped on the element (data-density, measured 'sm' under density="small") and the ambient scope channel keeps flowing for composed descendants, but NOTHING in the family reads it: zero density-carrier readers in ui/separator/ (grep receipt), the strip's 1px is engine-fixed. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'ROOT STAMP, ZERO-READER — the §11 carrier stamps --jx-size-effective on the element and the query() seat below rides THAT STAMP as its live receipt (13px → 18px across 48rem, measured); the strip itself never scales (blockSize 1px, engine-fixed) and zero size-effective readers exist (grep receipt). Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — zero shape-channel readers (grep receipt: no corner-shape or factor consumer in ui/separator/). Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — zero radius-effective readers (grep receipt); the masks do their own geometry (the dotted chain, the SVG wave) and a 1px strip has no corner to round. Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'ZERO-READ BY RULING — the INK LAW (Owner, 2026-09-01): a separator paints no COLOR, border-color is for borders. Six of seven variants read no token AT ALL (subtraction/blend physics); solid is the one token read in the family — the additive exception's var(--border) fill (grep receipt: tokens['--jx-border'] ×2 in the atoms, everything else token-free). Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'THE MECHANISM SPLIT, MEASURED (the frozen-ink seam's separator instance) — SIX variants are MECHANISM-FREE physics: the contrast ghost and the masks read the BACKDROP, not a token (auto-adaptive, flips nothing, freezes nothing), and the blend fade inverts by physics. ONE variant rides the TOKEN layer, and the ride is SCOPED: solid's fill is --jx-border, a :root-declared alias (--jx-border: var(--border)) whose var() substitutes AT THE ROOT — the light oklch(0 0 0) propagates pre-resolved, so a mid-tree .dark island CANNOT re-derive it (measured: the island flipped inherited --border to oklch(1 0 0) while the fill held oklch(0 0 0) — frozen); only root-level dark re-derives the alias (html.dark → oklch(1 0 0), measured). Root-pinned values cannot re-derive MID-TREE — and at root dark the unpinned solid flips while data-theme=light demo stages locally PIN (three tiers, the task-74 pin-finder; the W-next #7 shape narrowed to its mid-tree clause). The resolved dark theme still stamps class:dark for composed descendants. No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto'SUPPLY-ONLY — zero elevation-carrier readers (grep receipt); a 1px strip carries no shadow tier. Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'SUPPLY-ONLY, STATIC INK — zero motion-carrier readers (grep receipt); the family authors no animation and no transition (the ink is instantaneous physics — nothing to tween). Number unit: coefficient.

Receipts: the omission census (the component's bare hr carries data-jx-separator="fused" + data-orientation + class only — no density attr, no style attr, no role; vertical lands role=separator + aria-orientation AFTER the spread, not overridable), the managed stamp (data-density="sm" under density="small"), the variant ladder (blockSize 1px base / 2px dotted / 6px wavy; backdrop-filter contrast(0.5) on the ghost and its masks, none on solid and fade; difference blend on fade only), the ghost's auto-adaptivity (the strip pixel = contrast(0.5) of its ground, EXACT at both ends: ground 218 → strip 173 on a light ground (−45) and ground 58 → strip 93 on a dark ground (+35) — the fixed-point law 0.5C + 0.25 to the digit, sign flip pixel-measured at two device scale factors), the theme split (under a scoped .dark island the inherited --border flipped to oklch(1 0 0) while solid's fill HELD oklch(0 0 0) — the :root alias substitutes before inheritance — and at root dark the UNPINNED solid flips oklch(1 0 0) in place (the task-74 pin-finder; the earlier element-holds read superseded); the ghost's backdrop-filter byte-identical across both) and the query seat (the --jx-size-effective carrier flips 18px → 13px across the 48rem viewport key on the element's own style attr — the seat rides a full-width ground so the stamp is measurable at page scale) were measured on this page's served DOM (probe, task 40); the unread rows carry grep receipts over ui/separator/. LAW #19 id landscape: duplicate ids NONE page-wide; the keyed variants gallery mounts 7/7. The query() seat below rides the md viewport key (48rem) on the size lane.

one real query() case
<script lang="ts">
  import Separator from '@ui/separator.svelte';
  import { query } from '@lib/universal-props-query.svelte';
</script>

<!-- the md key is the registered VIEWPORT scale (48rem): below it the
     base (13px) stamps, at 48rem+ the md case (18px) wins — the
     carrier flips on the element while nothing in the family reads
     it (the zero-reader class): the stamp IS the receipt -->
<Separator size={query({ md: 18 }, 13)} />

The md key is the registered VIEWPORT scale (48rem): below it the base (13px) stamps; at 48rem and wider the md case wins (18px at a 1280 viewport, measured) — the carrier flips on the element's style attr while no family rule consumes it (the zero-reader class): the stamp is the receipt. The number lane goes bare.

Separator · universal props




separator-universal.svelte svelte
<!-- the eight-axis surface on the ink strip -->
<Separator density="small" />
<Separator size={14} variant="dashed" />

a11y

Accessibility

Horizontal needs no ARIA at all — the browser announces hr natively; vertical carries the WAI-ARIA separator pattern.

ARIA

AttributeValueDescription
hrnativeAnnounced as a separator/thematic break by the platform — zero wiring owed
roleseparatorOn the vertical path only (component-owned, not overridable)
aria-orientation"vertical"Set with the role on the vertical path
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h