Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

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.

Two Types
Decorative & Focusable
Native HTML Equivalent
<hr>
Focusable Keyboard
Arrows, Home, End

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)

Left Panel (50%)
Right Panel (50%)

Position: 50% / 50%

Vertical Splitter (Focusable)

Top Panel (50%)
Bottom Panel (50%)

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:

Decreases the separator value

Moves separator toward minimum

Increases the separator value

Moves separator toward maximum

Home
Sets separator to minimum value

Collapses the first panel to minimum size

End
Sets separator to maximum value

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-valuenow

Current position value (focusable separators only)

aria-valuemin

Minimum position value (focusable separators only)

aria-valuemax

Maximum position value (focusable separators only)

aria-valuetext

Human-readable description of position

aria-orientation

horizontal (default) or vertical

aria-controls

IDs of panels being separated

aria-label

Accessible name for the separator

aria-disabled

Indicates if splitter is disabled

Common Use Cases

Menu item groups (decorative)
Content section dividers (decorative)
Toolbar group separators (decorative)
Code editor split views (focusable)
File manager panes (focusable)
Email client layouts (focusable)
IDE panel resizers (focusable)
Dashboard widget separators (both)

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%".

Related Roles & Attributes

Specifications & Resources