Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

scrollbar

A graphical object that controls the scrolling of content within a viewing area. Scrollbars allow users to navigate through content that exceeds the visible area of a container.

Key Attributes
aria-valuenow, aria-controls
Orientation
vertical | horizontal
Keyboard Navigation
Arrows, Page Up/Down, Home/End

Overview

The scrollbar role identifies an element as a graphical object that controls scrolling within a content area. It represents the current scroll position as a value within a defined range (typically 0-100 representing percentage scrolled).

Scrollbars use the aria-controls attribute to reference the scrollable content they control. The aria-orientation attribute specifies whether the scrollbar is vertical or horizontal.

Native Scrollbars vs role="scrollbar"

Native browser scrollbars are automatically accessible and require no additional ARIA. Use role="scrollbar" only when creating fully custom scrollbar implementations. Consider using CSS scrollbar styling properties when possible, as they maintain native accessibility.

Live Demo: Scrollbar Interactions

Vertical Scrollbar

Content line 1 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 2 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 3 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 4 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 5 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 6 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 7 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 8 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 9 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 10 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 11 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 12 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 13 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 14 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 15 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 16 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 17 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 18 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 19 - This is sample scrollable content that demonstrates the scrollbar functionality.

Content line 20 - This is sample scrollable content that demonstrates the scrollbar functionality.

Position: 0%

Horizontal Scrollbar

Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10

Position: 0%

Try with keyboard: Focus the scrollbar, then use Arrow Keys to scroll incrementally, Page Up/Down for larger jumps, or Home/End to jump to start/end.

Code Examples

Basic Vertical Scrollbar

<!-- Basic Vertical Scrollbar -->
<div 
  role="scrollbar"
  aria-controls="content-area"
  aria-orientation="vertical"
  aria-valuenow="0"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Scroll content"
  tabindex="0"
>
  <div class="scrollbar-thumb"></div>
</div>

<div id="content-area" tabindex="0">
  <!-- Scrollable content here -->
</div>

<!-- Note: The scrollbar controls the content via aria-controls -->

Horizontal Scrollbar

<!-- Horizontal Scrollbar -->
<div 
  role="scrollbar"
  aria-controls="gallery"
  aria-orientation="horizontal"
  aria-valuenow="50"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Scroll gallery horizontally"
  tabindex="0"
>
  <div class="scrollbar-track">
    <div class="scrollbar-thumb" style="left: 50%"></div>
  </div>
</div>

<div id="gallery" class="horizontal-scroll-content">
  <!-- Gallery items -->
</div>

Keyboard Navigation

<!-- Scrollbar with Full Keyboard Support -->
<div 
  role="scrollbar"
  aria-controls="scrollable-content"
  aria-orientation="vertical"
  aria-valuenow="0"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuetext="0 percent scrolled"
  tabindex="0"
  id="custom-scrollbar"
>
  <div class="scrollbar-track">
    <div class="scrollbar-thumb"></div>
  </div>
</div>

<script>
  const scrollbar = document.getElementById('custom-scrollbar');
  const content = document.getElementById('scrollable-content');
  let currentValue = 0;
  const step = 10;
  const pageStep = 25;

  scrollbar.addEventListener('keydown', (e) => {
    let newValue = currentValue;

    switch (e.key) {
      case 'ArrowUp':
      case 'ArrowLeft':
        e.preventDefault();
        newValue = Math.max(0, currentValue - step);
        break;
      case 'ArrowDown':
      case 'ArrowRight':
        e.preventDefault();
        newValue = Math.min(100, currentValue + step);
        break;
      case 'PageUp':
        e.preventDefault();
        newValue = Math.max(0, currentValue - pageStep);
        break;
      case 'PageDown':
        e.preventDefault();
        newValue = Math.min(100, currentValue + pageStep);
        break;
      case 'Home':
        e.preventDefault();
        newValue = 0;
        break;
      case 'End':
        e.preventDefault();
        newValue = 100;
        break;
    }

    if (newValue !== currentValue) {
      currentValue = newValue;
      updateScrollbar();
    }
  });

  function updateScrollbar() {
    scrollbar.setAttribute('aria-valuenow', currentValue);
    scrollbar.setAttribute('aria-valuetext', currentValue + ' percent scrolled');
    
    // Update visual thumb position
    const thumb = scrollbar.querySelector('.scrollbar-thumb');
    thumb.style.top = currentValue + '%';
    
    // Scroll the content
    const maxScroll = content.scrollHeight - content.clientHeight;
    content.scrollTop = (currentValue / 100) * maxScroll;
  }
</script>

Native Scrollbar Styling (Preferred)

<!-- Using Native Scrollbar (Preferred) -->
<div class="scroll-container" tabindex="0">
  <div class="scroll-content">
    <!-- Content that overflows -->
    <p>Long content here...</p>
  </div>
</div>

<style>
  .scroll-container {
    height: 300px;
    overflow-y: auto;
    /* Custom scrollbar styling (limited cross-browser) */
  }

  /* Webkit browsers */
  .scroll-container::-webkit-scrollbar {
    width: 12px;
  }

  .scroll-container::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 6px;
  }

  .scroll-container::-webkit-scrollbar-thumb {
    background: #888;
    border-radius: 6px;
  }

  .scroll-container::-webkit-scrollbar-thumb:hover {
    background: #555;
  }

  /* Firefox */
  .scroll-container {
    scrollbar-width: thin;
    scrollbar-color: #888 #f1f1f1;
  }
</style>

<!-- Note: Native scrollbars are automatically accessible -->

React Component

// React Custom Scrollbar Component
import { useState, useRef, useCallback, useEffect } from 'react';

interface CustomScrollbarProps {
  orientation?: 'vertical' | 'horizontal';
  label: string;
  children: React.ReactNode;
}

function CustomScrollbar({
  orientation = 'vertical',
  label,
  children,
}: CustomScrollbarProps) {
  const [scrollPercent, setScrollPercent] = useState(0);
  const contentRef = useRef<HTMLDivElement>(null);
  const isVertical = orientation === 'vertical';

  const updateScrollFromContent = useCallback(() => {
    const content = contentRef.current;
    if (!content) return;

    const scrollable = isVertical
      ? content.scrollHeight - content.clientHeight
      : content.scrollWidth - content.clientWidth;

    if (scrollable > 0) {
      const scrolled = isVertical ? content.scrollTop : content.scrollLeft;
      setScrollPercent(Math.round((scrolled / scrollable) * 100));
    }
  }, [isVertical]);

  useEffect(() => {
    const content = contentRef.current;
    if (!content) return;

    content.addEventListener('scroll', updateScrollFromContent);
    return () => content.removeEventListener('scroll', updateScrollFromContent);
  }, [updateScrollFromContent]);

  const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
    const content = contentRef.current;
    if (!content) return;

    const step = 50;
    const pageStep = 200;

    switch (e.key) {
      case 'ArrowUp':
      case 'ArrowLeft':
        e.preventDefault();
        if (isVertical) content.scrollTop -= step;
        else content.scrollLeft -= step;
        break;
      case 'ArrowDown':
      case 'ArrowRight':
        e.preventDefault();
        if (isVertical) content.scrollTop += step;
        else content.scrollLeft += step;
        break;
      case 'PageUp':
        e.preventDefault();
        if (isVertical) content.scrollTop -= pageStep;
        else content.scrollLeft -= pageStep;
        break;
      case 'PageDown':
        e.preventDefault();
        if (isVertical) content.scrollTop += pageStep;
        else content.scrollLeft += pageStep;
        break;
      case 'Home':
        e.preventDefault();
        if (isVertical) content.scrollTop = 0;
        else content.scrollLeft = 0;
        break;
      case 'End':
        e.preventDefault();
        if (isVertical) {
          content.scrollTop = content.scrollHeight;
        } else {
          content.scrollLeft = content.scrollWidth;
        }
        break;
    }
  }, [isVertical]);

  return (
    <div className="scrollbar-container">
      <div
        ref={contentRef}
        className={`scroll-content ${isVertical ? 'vertical' : 'horizontal'}`}
        tabIndex={0}
      >
        {children}
      </div>
      
      <div
        role="scrollbar"
        aria-controls="scroll-content"
        aria-orientation={orientation}
        aria-valuenow={scrollPercent}
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuetext={`${scrollPercent} percent scrolled`}
        aria-label={label}
        tabIndex={0}
        onKeyDown={handleKeyDown}
        className={`custom-scrollbar ${isVertical ? 'vertical' : 'horizontal'}`}
      >
        <div 
          className="scrollbar-thumb"
          style={{
            [isVertical ? 'top' : 'left']: `${scrollPercent}%`,
          }}
        />
      </div>
    </div>
  );
}

// Usage
<CustomScrollbar orientation="vertical" label="Scroll article">
  <article>
    <h2>Long Article Content</h2>
    <p>Content here...</p>
  </article>
</CustomScrollbar>

Using aria-valuetext

<!-- Using aria-valuetext for Better Descriptions -->
<div 
  role="scrollbar"
  aria-controls="document"
  aria-orientation="vertical"
  aria-valuenow="25"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuetext="Page 1 of 4, 25% scrolled"
  aria-label="Document scroll position"
  tabindex="0"
>
  <div class="scrollbar-thumb"></div>
</div>

<!-- For documents with known sections -->
<div 
  role="scrollbar"
  aria-controls="document"
  aria-orientation="vertical"
  aria-valuenow="50"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuetext="Currently viewing: Chapter 3 - Getting Started"
  aria-label="Document navigation"
  tabindex="0"
>
  <div class="scrollbar-thumb"></div>
</div>

Keyboard Support

Scrolls content up/left by one increment

Direction depends on orientation

Scrolls content down/right by one increment

Direction depends on orientation

Page Up
Scrolls content up by a larger increment

Typically one viewport height

Page Down
Scrolls content down by a larger increment

Typically one viewport height

Home
Scrolls to the beginning of content

Sets aria-valuenow to aria-valuemin

End
Scrolls to the end of content

Sets aria-valuenow to aria-valuemax

Best Practices

Use native scrollbars when possible - they are automatically accessible

Always specify aria-controls to reference the scrollable content

Set aria-orientation to indicate vertical or horizontal scrolling

Provide aria-valuetext for more descriptive position announcements

Implement all expected keyboard interactions (arrows, Page Up/Down, Home/End)

Ensure the scrollbar thumb size indicates the viewport-to-content ratio

×

Don't create custom scrollbars unless absolutely necessary for design

×

Don't forget to update aria-valuenow when scroll position changes

×

Don't make scrollbars too thin to grab with a mouse or touch

×

Don't hide scrollbars completely - users need to know content is scrollable

Supported ARIA Attributes

aria-controlsRequired

References the ID of the scrollable content element

aria-valuenowRequired

Current scroll position value

aria-valueminRequired

Minimum scroll value (typically 0)

aria-valuemaxRequired

Maximum scroll value (typically 100)

aria-orientation

Specifies vertical or horizontal scrolling

aria-valuetext

Human-readable description of scroll position

aria-label

Accessible name for the scrollbar

aria-labelledby

References element(s) that label the scrollbar

Common Use Cases

Custom styled scrollable containers
Code editors and IDEs
Image galleries with scroll navigation
Virtual scrolling/infinite lists
Document viewers (PDF, ebook)
Timeline navigation controls
Custom select dropdowns
Chat/message windows

Accessibility Notes

Focus Management

The scrollbar should be focusable with tabindex="0". When focused, it receives keyboard events for scrolling. The scrollable content itself should also be focusable so users can scroll with arrow keys while reading.

Visual Indicators

Make the scrollbar thumb clearly visible against the track. Indicate when more content is available by showing the scrollbar. Consider showing scroll shadows at content edges to indicate scrollable areas.

Screen Reader Announcements

Screen readers announce scrollbars with their label, orientation, and current value. Use aria-valuetext to provide context like "Page 3 of 10" instead of just "30%".

Related Roles & Attributes

Specifications & Resources