Scrollbar

PreviousNext

A custom horizontal scrollbar component that syncs with scroll position via CSS scroll-timeline animations and supports pointer-driven interaction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Scrollbar } from '../ui/Scrollbar';

export function ScrollbarDemo() {
  return (
    <div>
      <Scrollbar timelineName="--scroller-outer" scrollbarWidth="60%">
        <div className="w-600 p-16">
          <div className="flex gap-8">
            {Array.from({ length: 20 }).map((_, i) => (
              <div
                key={i}
                className="flex h-100 w-100 flex-shrink-0 items-center justify-center rounded-8 bg-ultramarine text-white"
              >
                {i + 1}
              </div>
            ))}
          </div>
        </div>
      </Scrollbar>
      <div className="bg-ultramarine p-8 -m-8 mt-8">
        <Scrollbar variant="dark" timelineName="--scrollerDark-outer" scrollbarWidth="60%">
          <div className="w-600 p-16">
            <div className="flex gap-8">
              {Array.from({ length: 20 }).map((_, i) => (
                <div
                  key={i}
                  className="flex h-100 w-100 flex-shrink-0 items-center justify-center rounded-8 bg-white text-black"
                >
                  {i + 1}
                </div>
              ))}
            </div>
          </div>
        </Scrollbar>
      </div>
    </div>
  );
}

Usage

The Scrollbar component provides a custom horizontal scrollbar with automatic scroll synchronization and full pointer interaction:

  • Drag the nub to scroll to any position
  • Click the track outside the nub to jump to that position, then continue dragging
  • Touch targets are extended vertically (~16 px) for easier interaction on touch devices

Browser Support

Note: scroll-timeline is an experimental CSS feature. For older browsers, you'll need to include a scroll-timeline polyfill.

Basic Example

import { Scrollbar } from '@clubmed/trident-ui/ui/Scrollbar';
 
<Scrollbar>
  <div className="w-[900px]">
    {/* Scrollable content */}
  </div>
</Scrollbar>

Props

PropTypeDefaultDescription
childrenReactNode-The scrollable content
timelineNamestring'--scroller'Unique CSS timeline name for the scroll animation
scrollbarWidthstring'60%'Width of the scrollbar thumb as a percentage
variant'light' | 'dark''light'Color theme variant
classNamestring-Additional classes for the container
scrollerClassNamestring-Additional classes for the scrollable area
scrollbarClassNamestring-Additional classes for the scrollbar track
aria-labelstring'Scrollable content'Accessible label for the scrollable region

Custom Timeline Name

Use unique timeline names to support multiple independent scrollbars on the same page:

<Scrollbar timelineName="--header-scroller">
  <div className="w-[900px]">Header content</div>
</Scrollbar>
 
<Scrollbar timelineName="--body-scroller">
  <div className="w-[1200px]">Body content</div>
</Scrollbar>

Scrollbar Thumb Width

Control the scrollbar thumb width with the scrollbarWidth prop:

<Scrollbar scrollbarWidth="40%">
  <div className="w-[900px]">Content</div>
</Scrollbar>

Dark Variant

Use the variant prop for different color themes:

<Scrollbar variant="dark">
  <div className="w-[900px]">Content</div>
</Scrollbar>

Accessibility

The component includes proper ARIA roles and attributes:

  • role="region" on the scrollable area with customizable aria-label
  • role="scrollbar" on the scrollbar track with orientation and value attributes
  • Keyboard navigation support with tabIndex={0} on the scrollable area

CSS-Only Implementation

If you prefer not to use the React component, you can implement the scrollbar manually using CSS scroll-timeline properties. This approach provides automatic scroll synchronization but without the interactive drag functionality.

Manual Setup

The scrollbar requires three elements with specific timeline properties:

  1. Container - Defines the timeline scope
  2. Scroller (data-scroller) - The scrollable container that creates the scroll timeline
  3. Scrollbar (data-scrollbar) - The visual scrollbar with animated thumb
<div style={{ timelineScope: '--my-scroller' }}>
  <div 
    data-scroller
    style={{ scrollTimeline: '--my-scroller x' }}
  >
    <div className="w-[900px]">
      {/* Scrollable content */}
    </div>
  </div>
  <div 
    data-scrollbar
    style={{ '--scrollbar-width': '60%' } as React.CSSProperties}
  >
    <div 
      data-scrollprogress 
      style={{ animationTimeline: '--my-scroller' }}
    />
  </div>
</div>

Timeline Name Requirements

Each scroller needs a unique timeline name (e.g., --my-scroller) that must be:

  • Set on the container with timelineScope: '--my-scroller'
  • Used in the scroller's scrollTimeline property with the x axis: '--my-scroller x'
  • Referenced in the scrollprogress element's animationTimeline: '--my-scroller'

This allows multiple scrollbars to coexist on the same page, including nested scrollers.

Using Tailwind Classes

You can also use Tailwind's arbitrary value syntax:

<div className="[timeline-scope:--my-scroller]">
  <div 
    data-scroller
    className="[scroll-timeline:--my-scroller_x]"
  >
    <div className="w-[900px]">Content</div>
  </div>
  <div data-scrollbar>
    <div 
      data-scrollprogress 
      className="[animation-timeline:--my-scroller]"
    />
  </div>
</div>

Dark Variant (CSS-Only)

For a dark themed scrollbar, add data-scroller="dark":

<div style={{ timelineScope: '--my-scroller' }}>
  <div 
    data-scroller="dark"
    style={{ scrollTimeline: '--my-scroller x' }}
  >
    {/* content */}
  </div>
  <div data-scrollbar>
    <div 
      data-scrollprogress 
      style={{ animationTimeline: '--my-scroller' }}
    />
  </div>
</div>

Component vs CSS-Only

The Scrollbar component provides the same visual result as the CSS-only approach but with additional benefits:

  • Proper ARIA attributes for accessibility
  • Cleaner, more maintainable API
  • Encapsulated timeline name management
  • Pointer interaction: drag nub, click track, extended touch targets

Use the component for better accessibility and developer experience, or use the CSS-only approach for maximum flexibility and minimal JavaScript. Note that the CSS-only approach does not include pointer-driven scrolling.