separator
A divider that separates and distinguishes sections of content or groups of menu items. Separators can be decorative or focusable when used as splitters for resizable panels.
Overview
The separator role has two distinct uses depending on whether it is focusable:
- Decorative separator: A static visual divider between content sections (like an
<hr>). Not focusable, no value attributes needed. - Focusable separator (splitter): An interactive control that allows users to resize adjacent panels. Requires
tabindex="0"and value attributes.
When to Use Each Type
Use decorative separators for visual organization in menus, toolbars, and content sections. Use focusable separators (splitters) when building resizable layouts like code editors, file managers, or split-view interfaces.
Live Demo: Separator Types
Decorative Separator
This is the first section of content.
This is the second section of content, visually separated from the first.
↑ A simple <hr> element that is not focusable
Horizontal Splitter (Focusable)
Position: 50% / 50%
Vertical Splitter (Focusable)
Position: 50% / 50%
Try with keyboard: Focus a splitter (Tab to it), then use Arrow Keys to resize panels, Home for minimum, or End for maximum.
Code Examples
Decorative Separator
<!-- Decorative Separator (not focusable) -->
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/services">Services</a>
</nav>
<hr />
<main>
<h1>Welcome to Our Site</h1>
<p>Content here...</p>
</main>
<!-- The native <hr> element is automatically a separator -->
<!-- For custom separators: -->
<div role="separator" aria-orientation="horizontal"></div>
<!-- Note: Decorative separators should NOT have tabindex -->Focusable Separator (Splitter)
<!-- Focusable Separator (Splitter/Resizer) -->
<div class="split-view">
<div id="left-panel" class="panel">
Left content
</div>
<div
role="separator"
aria-valuenow="50"
aria-valuemin="10"
aria-valuemax="90"
aria-orientation="vertical"
aria-label="Resize panels"
aria-controls="left-panel right-panel"
tabindex="0"
class="splitter"
>
<div class="splitter-handle"></div>
</div>
<div id="right-panel" class="panel">
Right content
</div>
</div>
<!-- Focusable separators require:
- tabindex="0"
- aria-valuenow, aria-valuemin, aria-valuemax
- aria-controls (optional but recommended) -->Keyboard Navigation
<!-- Splitter with Full Keyboard Support -->
<div class="split-container">
<div id="panel-a">Panel A</div>
<div
role="separator"
id="splitter"
aria-valuenow="50"
aria-valuemin="10"
aria-valuemax="90"
aria-orientation="horizontal"
aria-label="Adjust panel sizes"
aria-controls="panel-a panel-b"
aria-valuetext="Panel A: 50%, Panel B: 50%"
tabindex="0"
></div>
<div id="panel-b">Panel B</div>
</div>
<script>
const splitter = document.getElementById('splitter');
const panelA = document.getElementById('panel-a');
const panelB = document.getElementById('panel-b');
let currentValue = 50;
const step = 5;
splitter.addEventListener('keydown', (e) => {
let newValue = currentValue;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newValue = Math.max(10, currentValue - step);
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newValue = Math.min(90, currentValue + step);
break;
case 'Home':
e.preventDefault();
newValue = 10;
break;
case 'End':
e.preventDefault();
newValue = 90;
break;
}
if (newValue !== currentValue) {
currentValue = newValue;
updateSplitter();
}
});
function updateSplitter() {
splitter.setAttribute('aria-valuenow', currentValue);
splitter.setAttribute('aria-valuetext',
'Panel A: ' + currentValue + '%, Panel B: ' + (100 - currentValue) + '%'
);
panelA.style.width = currentValue + '%';
panelB.style.width = (100 - currentValue) + '%';
}
</script>Native HTML Elements
<!-- Using Native HTML Elements -->
<!-- Horizontal rule - automatically has separator role -->
<p>First paragraph of content.</p>
<hr />
<p>Second paragraph of content.</p>
<!-- Thematic break with styling -->
<hr class="fancy-separator" />
<style>
hr {
border: 0;
height: 1px;
background: linear-gradient(to right, transparent, #888, transparent);
margin: 2rem 0;
}
.fancy-separator {
height: 3px;
background: linear-gradient(to right, #4f46e5, #7c3aed, #4f46e5);
border-radius: 2px;
}
</style>
<!-- For visual-only decoration that shouldn't be announced: -->
<div role="presentation" class="decorative-line"></div>
<!-- or -->
<div aria-hidden="true" class="decorative-line"></div>React Splitter Component
// React Splitter Component
import { useState, useRef, useCallback, useEffect } from 'react';
interface SplitterProps {
orientation?: 'horizontal' | 'vertical';
initialPosition?: number;
minPosition?: number;
maxPosition?: number;
onResize?: (position: number) => void;
children: [React.ReactNode, React.ReactNode];
}
function Splitter({
orientation = 'horizontal',
initialPosition = 50,
minPosition = 10,
maxPosition = 90,
onResize,
children,
}: SplitterProps) {
const [position, setPosition] = useState(initialPosition);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const isHorizontal = orientation === 'horizontal';
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
const step = 5;
let newPosition = position;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newPosition = Math.max(minPosition, position - step);
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newPosition = Math.min(maxPosition, position + step);
break;
case 'Home':
e.preventDefault();
newPosition = minPosition;
break;
case 'End':
e.preventDefault();
newPosition = maxPosition;
break;
default:
return;
}
setPosition(newPosition);
onResize?.(newPosition);
}, [position, minPosition, maxPosition, onResize]);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(true);
}, []);
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const newPosition = isHorizontal
? ((e.clientX - rect.left) / rect.width) * 100
: ((e.clientY - rect.top) / rect.height) * 100;
const clampedPosition = Math.max(minPosition, Math.min(maxPosition, newPosition));
setPosition(clampedPosition);
onResize?.(clampedPosition);
};
const handleMouseUp = () => setIsDragging(false);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, isHorizontal, minPosition, maxPosition, onResize]);
const valueText = isHorizontal
? `Left panel: ${Math.round(position)}%, Right panel: ${Math.round(100 - position)}%`
: `Top panel: ${Math.round(position)}%, Bottom panel: ${Math.round(100 - position)}%`;
return (
<div
ref={containerRef}
className={`splitter-container ${isHorizontal ? 'horizontal' : 'vertical'}`}
style={{
display: 'flex',
flexDirection: isHorizontal ? 'row' : 'column',
}}
>
<div style={{ [isHorizontal ? 'width' : 'height']: `${position}%` }}>
{children[0]}
</div>
<div
role="separator"
aria-valuenow={Math.round(position)}
aria-valuemin={minPosition}
aria-valuemax={maxPosition}
aria-orientation={orientation}
aria-valuetext={valueText}
aria-label="Resize panels"
tabIndex={0}
onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
className={`splitter-handle ${isDragging ? 'dragging' : ''}`}
/>
<div style={{ [isHorizontal ? 'width' : 'height']: `${100 - position}%` }}>
{children[1]}
</div>
</div>
);
}
// Usage
<Splitter orientation="horizontal" initialPosition={30}>
<aside>Sidebar content</aside>
<main>Main content</main>
</Splitter>Menu Separator
<!-- Separator in Menu Context -->
<ul role="menu" aria-label="Edit menu">
<li role="menuitem">Undo</li>
<li role="menuitem">Redo</li>
<li role="separator"></li>
<li role="menuitem">Cut</li>
<li role="menuitem">Copy</li>
<li role="menuitem">Paste</li>
<li role="separator"></li>
<li role="menuitem">Select All</li>
</ul>
<!-- In menus, separators group related items -->
<!-- They are NOT focusable and have no value attributes -->Keyboard Support (Focusable Separators Only)
Decorative separators are not focusable and have no keyboard interaction. Focusable separators (splitters) support the following keys:
Moves separator toward minimum
Moves separator toward maximum
Collapses the first panel to minimum size
Expands the first panel to maximum size
Best Practices
Use native <hr> element for decorative separators when possible
Only add tabindex="0" when the separator is interactive (splitter)
Include aria-valuenow, aria-valuemin, aria-valuemax for focusable separators
Use aria-valuetext to describe panel sizes in human-readable format
Provide visual feedback when splitter is focused or being dragged
Use aria-orientation to indicate horizontal or vertical separator
Don't make decorative separators focusable
Don't omit value attributes from focusable separators
Don't use separators for purely visual decoration - use CSS borders/lines
Don't forget keyboard support for splitters
Supported ARIA Attributes
aria-valuenowCurrent position value (focusable separators only)
aria-valueminMinimum position value (focusable separators only)
aria-valuemaxMaximum position value (focusable separators only)
aria-valuetextHuman-readable description of position
aria-orientationhorizontal (default) or vertical
aria-controlsIDs of panels being separated
aria-labelAccessible name for the separator
aria-disabledIndicates if splitter is disabled
Common Use Cases
Accessibility Notes
Decorative vs Focusable
The presence of tabindex determines whether a separator is decorative or interactive. Screen readers treat focusable separators as sliders with adjustable values, while decorative separators are announced briefly as content dividers.
Visual Affordance
For splitters, provide clear visual cues that they are draggable: cursor changes (col-resize/row-resize), hover effects, and visible handles. Users should be able to easily identify what can be resized.
Screen Reader Behavior
Focusable separators are announced as sliders: "[label], separator, [value]". Screen readers will also announce value changes as users adjust the position. Use aria-valuetext to provide context like "Sidebar: 30%, Main content: 70%".

