Separator
A hairline rule that divides content horizontally or vertically.
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-borderordivide-yon 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
Space first, rule second
Anatomy#
- Rule. 1px of Hairline (
--border), full width when horizontal, or full height of its row when vertical. - 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#
| State | Treatment |
|---|---|
| Horizontal | h-px w-full. The default orientation. |
| Vertical | w-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">witharia-orientationanddata-orientation, which thedata-horizontal:anddata-vertical:classes read. - It doesn't take focus and isn't interactive.
shrink-0keeps 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
Northwind Freight
Net 30, ACH
ap@northwindfreight.com
18 open invoices
Halcyon Logistics
Balance due $18,240.00
shadow-border and split its inside with a separator.Halcyon Logistics
Balance due $18,240.00
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#
| Token | Used for |
|---|---|
--border | The 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">).
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Draws a 1px row or a 1px column, and sets aria-orientation. |
className | string | No default | Merged after the base classes. Use data-vertical:h-4 and data-vertical:self-center for a short vertical rule, or margins for spacing. |
render | ReactElement | (props, state) => ReactElement | No default | Render 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.