Separator
Mark a meaningful content boundary without adding a fake action or redundant announcement.
Semantic boundary or decoration
Use Separator when a content boundary benefits from explicit structure. Prefer headings/spacing when they already communicate the relationship. A thin line alone does not name a section or create an action.
The pinned primitive is semantic by default. It has orientation and native/render props, without a decorative boolean. Use aria-hidden=true for redundant decorative boundaries while retaining meaningful text structure.
Anatomy and dimensions
| Part | Contract |
|---|---|
| Horizontal | Styled installed div/ref, 1px high and full available width, background border role. |
| Vertical | 1px width, automatic height and align-self:stretch. Needs a parent with meaningful cross-axis height; a line cannot create its own content height. |
| Semantics | Installed separator role and orientation. No focus/press/arrow interaction is supplied. |
| Customization | State-based className callback now composes with mt-separator; native/render/style props belong to the installed primitive. |
| Contrast | If a boundary is essential to understanding, verify nontext contrast or supply independent structural cues. The border role is not a blanket guarantee for every enclosing surface. |
Material divider hierarchy, insets and semantic purpose
The reference says to prefer open space when it already groups content: use a visible but subtle line for meaningful hierarchy, rather than a divider after every repeated item. Full-width lines distinguish larger unrelated sections or interactive versus noninteractive areas; inset lines clarify related/nested groups. Vertical divisions suit larger side-by-side arrangements.
| Reference point | Web choice and verification boundary |
|---|---|
| Native geometry versus local CSS | Reference width 100%, leading inset 16dp, optional trailing inset 16dp,4dp supporting gap and 8dp surrounding margins are not local props or converted values. This Separator supplies orientation and a1px token-colored line; the caller owns logical insets/spacing and actual group structure. |
| Decoration and semantics | The native accessibility page calls dividers decorative with no contrast minimum. The installed Separator can instead expose a meaningful nonfocusable separator boundary, or be hidden when purely decorative. Explain the purpose and choose the actual semantic contract; a line alone never replaces headings or labels. |
| Orientation and direction | Use logical start/end insets so RTL preserves grouping. A passive vertical separator is not a draggable splitter and has no keyboard action. Resizable pane handles are a separate interactive component with their own names, targets and keyboard behavior. |
| Contrast and clutter | Do not impose a decorative contrast threshold as if it were a control. An essential visual boundary still needs to remain understandable, including forced colors and high contrast. Prefer clear group spacing and restrained use over heavier repeated lines. |
| Verification | The working orientation/decorative controls preserve both content groups. Test actual geometry, absence from Tab order, hidden versus semantic mode, contained width and unchanged reading order. Decorative silence and spoken grouping remain target assistive-technology checks. |
Real orientation and decoration
Switch the local divider between horizontal and vertical while keeping both content groups. Decorative mode hides the redundant semantic line; the explanatory text remains visible. This example has no pretend menu items or pointer-only commands.
English-only draft details.
English-only local result.
Default is a semantic separator; decorative mode hides this redundant boundary from the accessibility tree. The copy remains visible.
'use client';
import {useState,type CSSProperties} from 'react';
import {Stack,HStack,VStack,AspectRatio,Separator,VisuallyHidden,Button} from '@mobtranslate/ui';
const action:CSSProperties={minWidth:0,maxWidth:'100%',minHeight:44,height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
export function StackLayoutExample(){
const [row,setRow]=useState(false);
const [wrap,setWrap]=useState(true);
const [gap,setGap]=useState(3);
const [rtl,setRtl]=useState(false);
const [selected,setSelected]=useState('overview');
const tasks=[{id:'overview',label:'Local overview',text:'Actual English-only overview content.'},{id:'notes',label:'Local reading notes',text:'Actual English-only reading-note content.'},{id:'saved',label:'Local saved examples',text:'Actual English-only saved-example content.'}];
return <VStack data-stack-lab gap={4}>
<HStack gap={3} wrap><Button style={action} type="button" variant="outline" aria-pressed={row} onClick={()=>setRow(v=>!v)}>Use row task layout</Button><Button style={action} type="button" variant="outline" aria-pressed={wrap} onClick={()=>setWrap(v=>!v)}>Allow task wrapping</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL task layout</Button></HStack>
<label>Gap in quarter-rem units <select data-stack-gap className="mt-input" value={gap} onChange={event=>setGap(Number(event.target.value))}><option value={0}>0</option><option value={3}>3</option><option value={6}>6</option></select></label>
<Stack data-stack-tasks direction={row?'row':'column'} wrap={wrap} gap={gap} align="stretch" dir={rtl?'rtl':'ltr'}>{tasks.map(task=><Button style={{...action,flex:row?'1 1 10rem':undefined}} key={task.id} type="button" variant={selected===task.id?'primary':'outline'} aria-pressed={selected===task.id} onClick={()=>setSelected(task.id)}>{task.label}</Button>)}</Stack><p data-stack-result>{tasks.find(task=>task.id===selected)?.text}</p>
<p>These pressed buttons select one local preview. Stack supplies layout only; it adds no toolbar, tab or arrow-key model.</p>
</VStack>;
}
export function AspectFrameExample(){
const [ratio,setRatio]=useState(16/9);
const [cover,setCover]=useState(false);
return <div data-aspect-lab className="space-y-4"><label>Frame width divided by height <select data-aspect-ratio className="mt-input" value={ratio} onChange={event=>setRatio(Number(event.target.value))}><option value={16/9}>16:9</option><option value={4/3}>4:3</option><option value={1}>1:1</option><option value={3/4}>3:4</option></select></label><Button style={action} type="button" variant="outline" aria-pressed={cover} onClick={()=>setCover(v=>!v)}>Crop illustration to cover frame</Button>
<figure><AspectRatio ratio={ratio} data-aspect-frame><svg viewBox="0 0 400 200" preserveAspectRatio={cover?'xMidYMid slice':'xMidYMid meet'} role="img" aria-label="English-only geometric illustration with three numbered regions" style={{background:'var(--color-muted)'}}><rect x="0" y="0" width="400" height="200" fill="var(--color-muted)"/><rect x="20" y="20" width="100" height="160" fill="var(--color-background)" stroke="var(--color-foreground)"/><circle cx="200" cy="100" r="60" fill="var(--color-primary)"/><rect x="280" y="20" width="100" height="160" fill="var(--color-background)" stroke="var(--color-foreground)"/><text x="70" y="110" textAnchor="middle" fill="var(--color-foreground)" fontSize="32">1</text><text x="200" y="110" textAnchor="middle" fill="var(--color-primary-foreground)" fontSize="32">2</text><text x="330" y="110" textAnchor="middle" fill="var(--color-foreground)" fontSize="32">3</text></svg></AspectRatio><figcaption className="mt-3">Local geometric illustration, not a photograph. {cover?'Cover can crop edge regions.':'Contain preserves all three regions.'} Caption stays outside the clipped media frame.</figcaption></figure>
</div>;
}
export function SeparatorSectionsExample(){
const [vertical,setVertical]=useState(false);
const [decorative,setDecorative]=useState(false);
return <div data-separator-lab className="space-y-4"><HStack wrap gap={3}><Button style={action} type="button" variant="outline" aria-pressed={vertical} onClick={()=>setVertical(v=>!v)}>Use vertical section divider</Button><Button style={action} type="button" variant="outline" aria-pressed={decorative} onClick={()=>setDecorative(v=>!v)}>Make local divider decorative</Button></HStack><Stack direction={vertical?'row':'column'} gap={3} style={vertical?{minHeight:100}:undefined}><p style={{flex:1}}>English-only draft details.</p><Separator data-local-divider orientation={vertical?'vertical':'horizontal'} aria-hidden={decorative||undefined} className={state=>state.orientation==='vertical'?'divider-vertical':'divider-horizontal'} /><p style={{flex:1}}>English-only local result.</p></Stack><p>Default is a semantic separator; decorative mode hides this redundant boundary from the accessibility tree. The copy remains visible.</p></div>;
}
export function HiddenNameExample(){
const [saved,setSaved]=useState(false);
return <div data-hidden-lab className="space-y-4"><p>An icon control has a real text name in a visually hidden span. Visible help remains available for everyone.</p><Button id="hidden-bookmark" type="button" size="icon" variant="outline" aria-pressed={saved} onClick={()=>setSaved(v=>!v)}><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill={saved?'currentColor':'none'} stroke="currentColor"><path d="M6 3h12v18l-6-4-6 4z"/></svg><VisuallyHidden data-hidden-name>Toggle local example bookmark</VisuallyHidden></Button><p role="status" data-hidden-result>{saved?'Actual local bookmark present.':'Actual local bookmark absent.'}</p><p>Keep interactive controls outside VisuallyHidden. Clipping a focusable link would leave an invisible keyboard target.</p></div>;
}
Complete exported API
| Prop | Installed behavior |
|---|---|
| orientation | horizontal or vertical, default horizontal. A data-orientation attribute drives the actual geometry. |
| aria-hidden | Native attribute for a redundant decorative boundary. It does not hide adjacent content. |
| className / style / render | Installed state-aware customization and native attributes. Preserve role/orientation if rendering a semantic boundary through another element. |
| ref | HTMLDivElement ref for the default part. A custom render must preserve the actual ref contract. |
Keyboard and composition
- A static separator is not a tab stop. Do not make a passive rule focusable or add cursor:pointer without a real action.
- Toolbar/menu separators belong to their respective composite APIs when they need registered relationships. This general separator does not turn a Stack into a toolbar.
- Check vertical stretch and long text in the real parent. Orientation describes the line, not the reading order of surrounding content.
- Avoid repeated decorative announcements between every list row. Use meaningful list/table semantics and spacing instead of adding a role for each visual boundary.
Verification
Run test:layout-utilities, checked-source/token freshness, lint, UI/styleguide types, production build and the complete route/fragment crawl. Verify real geometry, DOM order, accessible names and task outcomes; browser evidence does not certify every consuming layout or spoken assistive-technology/device combination.
- Verify actual orientation/geometry, state class callbacks, semantic versus hidden mode and unchanged neighboring content. Accessible-tree/spoken behavior requires its own platform checks.