Skip to content

Separator

A hairline rule that divides content horizontally or vertically.

Status
Beta
Level
Atom
Category
Layout
Adoption
Not used yet
import { Separator } from "@oration/canon/components/separator";
packages/canon/src/components/separator.tsx

Remittance details

ap@northwindfreight.com

PDF advice after every payment run

Bank account

Bank of America, checking

Account ending 4417, verified Sep 14

import { Button } from "@oration/canon/components/button";import { Separator } from "@oration/canon/components/separator";import { toast } from "@oration/canon/components/toast";export function Hero() {    return (        <div className="w-full max-w-md rounded-xl bg-card p-4 text-left shadow-border">            <div className="flex items-start justify-between gap-4">                <div className="flex flex-col gap-1">                    <p className="text-sm font-semibold">Remittance details</p>                    <p className="text-13 text-muted-foreground">                        ap@northwindfreight.com                    </p>                    <p className="text-13 text-muted-foreground">                        PDF advice after every payment run                    </p>                </div>                <Button                    type="button"                    variant="ghost"                    size="sm"                    onClick={() =>                        toast.add({ title: "Editing remittance details" })                    }                >                    Edit                </Button>            </div>            <Separator className="my-4" />            <div className="flex items-start justify-between gap-4">                <div className="flex flex-col gap-1">                    <p className="text-sm font-semibold">Bank account</p>                    <p className="text-13 text-muted-foreground">                        Bank of America, checking                    </p>                    <p className="text-13 text-muted-foreground tabular-nums">                        Account ending 4417, verified Sep 14                    </p>                </div>                <Button                    type="button"                    variant="ghost"                    size="sm"                    onClick={() => toast.add({ title: "Editing bank account" })}                >                    Edit                </Button>            </div>        </div>    );}

Usage#

Separator is a 1px Hairline rule that divides content horizontally, or vertically between items in a row. It renders a Base UI Separator, so it is exposed as role="separator" with its orientation. In Oration hairlines are structural: they split sections inside a surface, rule table rows and divide groups in a toolbar or header. The mistake is decoration: using a separator to outline a card, or ruling content that spacing already groups.

When to use

  • To split two sections inside one card or sheet, such as remittance details and bank account.
  • Vertically, between the sidebar toggle and the breadcrumbs in a page header, at 16px tall.
  • Between groups of controls in a toolbar, so the groups read as separate.
  • Between groups of items in a list or menu, where a heading would be too much.

When not to use

  • As the edge of a card, panel or popover. Raised surfaces take their edge from the hairline lift shadow. Use Card
  • For a labelled split such as the or between two sign-in options. Use Field
  • Between rows of a table or list you build. A border-b border-border or divide-y on the container is simpler and draws the same line. Use Table
  • For a quiet note that also divides, with an icon or a short label. Use Marker
  • Between buttons joined into one control. Use Button group

The Hairline-and-Lift Rule

A raised surface takes its edge and its lift from one composite shadow, never from a CSS border or a separator plus a shadow. Hairlines are for structural dividers only: table rules, header bottoms and section splits.

Space first, rule second

Most groups are separated well enough by 12 to 32px of space. Add a separator when two regions sit flush, or when spacing alone would leave it unclear where one ends.

Anatomy#

SuppliersNorthwind Freight
  1. Rule. 1px of Hairline (--border), full width when horizontal, or full height of its row when vertical.
  2. Spacing. Not part of the component. Add margin with the layout: 16px around a section split in a card, 4 to 8px beside a vertical separator in a header or toolbar.

Examples#

Horizontal

The default. One hairline with 16px of space either side splits a card into two sections, here the supplier's terms and its balance.

Payment terms
Net 30
Method
ACH
Open invoices
18
Balance due
$42,615.25
import { Separator } from "@oration/canon/components/separator";export function Horizontal() {    return (        <div className="w-full max-w-sm rounded-xl bg-card p-4 shadow-border">            <dl className="grid grid-cols-[8rem_minmax(0,1fr)] gap-y-2 text-13">                <dt className="text-muted-foreground">Payment terms</dt>                <dd>Net 30</dd>                <dt className="text-muted-foreground">Method</dt>                <dd>ACH</dd>            </dl>            <Separator className="my-4" />            <dl className="grid grid-cols-[8rem_minmax(0,1fr)] gap-y-2 text-13">                <dt className="text-muted-foreground">Open invoices</dt>                <dd className="tabular-nums">18</dd>                <dt className="text-muted-foreground">Balance due</dt>                <dd className="tabular-nums">$42,615.25</dd>            </dl>        </div>    );}

Vertical

The page header pattern: a 16px vertical rule between the sidebar toggle and the breadcrumbs. data-vertical:h-4 shortens it and data-vertical:self-center stops it stretching to the row's height.

import { Button } from "@oration/canon/components/button";import { Separator } from "@oration/canon/components/separator";import { toast } from "@oration/canon/components/toast";import { Tooltip, TooltipContent, TooltipTrigger } from "@oration/canon/components/tooltip";import { ChevronRightIcon, PanelLeftIcon } from "lucide-react";export function Vertical() {    return (        <div className="flex h-12 w-full max-w-lg items-center gap-2 border-b border-border bg-background px-3">            <Tooltip>                <TooltipTrigger                    render={                        <Button                            type="button"                            variant="ghost"                            size="icon-sm"                            aria-label="Toggle sidebar"                            onClick={() =>                                toast.add({ title: "Sidebar collapsed" })                            }                        />                    }                >                    <PanelLeftIcon aria-hidden="true" />                </TooltipTrigger>                <TooltipContent>Toggle sidebar</TooltipContent>            </Tooltip>            <Separator                orientation="vertical"                className="mr-1 data-vertical:h-4 data-vertical:self-center"            />            <nav                aria-label="Breadcrumb"                className="flex min-w-0 items-center gap-1 text-13"            >                <span className="text-muted-foreground">Suppliers</span>                <ChevronRightIcon                    aria-hidden="true"                    className="size-3.5 text-muted-foreground"                />                <span className="truncate font-medium" aria-current="page">                    Northwind Freight                </span>            </nav>        </div>    );}

In a toolbar

Inside a role="toolbar", a vertical separator tells sighted and screen reader users where one group of controls ends: filter and sort, then export.

import { Button } from "@oration/canon/components/button";import { Separator } from "@oration/canon/components/separator";import { toast } from "@oration/canon/components/toast";import { ArrowDownUpIcon, DownloadIcon, FilterIcon } from "lucide-react";export function InToolbar() {    return (        <div            role="toolbar"            aria-label="Invoice list"            className="flex h-10 items-center gap-1 rounded-xl bg-card px-1.5 shadow-border"        >            <Button                type="button"                variant="ghost"                size="sm"                onClick={() => toast.add({ title: "Filter opened" })}            >                <FilterIcon data-icon="inline-start" aria-hidden="true" />                Filter            </Button>            <Button                type="button"                variant="ghost"                size="sm"                onClick={() => toast.add({ title: "Sorted by due date" })}            >                <ArrowDownUpIcon data-icon="inline-start" aria-hidden="true" />                Sort            </Button>            <Separator                orientation="vertical"                className="mx-1 data-vertical:h-5 data-vertical:self-center"            />            <Button                type="button"                variant="ghost"                size="sm"                onClick={() =>                    toast.add({                        title: "Exporting 212 invoices",                        description: "The CSV downloads when it's ready.",                    })                }            >                <DownloadIcon data-icon="inline-start" aria-hidden="true" />                Export CSV            </Button>        </div>    );}

Between groups

In a list of views, a separator with 4px of space either side divides the working views from the archive, where a heading would be too much.

import { Separator } from "@oration/canon/components/separator";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function BetweenGroups() {    const [view, setView] = React.useState("All invoices");    const groups = [        ["All invoices", "Awaiting approval", "Exceptions"],        ["Scheduled", "Paid"],    ];    return (        <nav            aria-label="Invoice views"            className="flex w-56 flex-col rounded-xl bg-card p-1 shadow-border"        >            {groups.map((group, index) => (                <React.Fragment key={group[0]}>                    {index > 0 ? <Separator className="my-1" /> : null}                    {group.map((item) => (                        <button                            key={item}                            type="button"                            aria-current={view === item ? "page" : undefined}                            onClick={() => setView(item)}                            className={cn(                                "flex h-8 items-center rounded-lg px-2 text-left text-13 transition-colors duration-150 hover:bg-muted",                                view === item && "bg-muted font-medium",                            )}                        >                            {item}                        </button>                    ))}                </React.Fragment>            ))}        </nav>    );}

States#

States
StateTreatment
Horizontalh-px w-full. The default orientation.
Verticalw-px self-stretch. It fills the height of its flex row; set data-vertical:h-4 data-vertical:self-center for a shorter rule.

Behavior#

  • Renders a <div role="separator"> with aria-orientation and data-orientation, which the data-horizontal: and data-vertical: classes read.
  • It doesn't take focus and isn't interactive.
  • shrink-0 keeps it from collapsing in a flex row.
  • In dark, the Hairline is white at 7.5%, so the rule stays faint on both planes.

Do and don't#

Northwind Freight

Net 30, ACH

ap@northwindfreight.com

18 open invoices

Do. Use one hairline to split two distinct sections inside a card.

Northwind Freight

Net 30, ACH

ap@northwindfreight.com

18 open invoices

Don't. Rule every line of a short list whose spacing already groups it. The page turns into a ledger of lines.

Halcyon Logistics

Balance due $18,240.00

Do. Lift a card with shadow-border and split its inside with a separator.

Halcyon Logistics

Balance due $18,240.00

Don't. Draw the card's edge with borders or separators. It breaks The Hairline-and-Lift Rule and doubles the edge.

Content#

  • A separator has no text. If the split needs a word, use a heading, a Field separator or a Marker.

Accessibility#

  • Screen readers may announce separator. Use it where the split is meaningful, such as between groups of toolbar controls.
  • For a purely visual line inside a component that already groups its content, a CSS border avoids an extra announcement.
  • Inside a role="toolbar", separators tell assistive tech where one group of controls ends.
  • Don't rely on the rule alone to show grouping; keep the spacing and headings that make the structure clear.

Design tokens#

Design tokens
TokenUsed for
--borderThe 1px Hairline (white at 7.5% in dark)

API reference#

Separator

A Base UI Separator styled as a Hairline. Renders data-slot="separator".

Other props spread onto Base UI Separator (<div role="separator">).

Props of Separator
PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Draws a 1px row or a 1px column, and sets aria-orientation.
classNamestringNo defaultMerged after the base classes. Use data-vertical:h-4 and data-vertical:self-center for a short vertical rule, or margins for spacing.
renderReactElement | (props, state) => ReactElementNo defaultRender as another element, such as an <hr> or an <li> in a list.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

Only one product screen, the page header, uses Separator. Elsewhere apps/web draws the same line with border-t border-border, border-b border-border or divide-y (over 230 files).

There is no decorative option: the rule is always exposed as role="separator", even where it is only visual.