- Accordion
- Advanced Toast
- AlertMessage
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- Chapters
- ChatBody
- ChatButton
- ChatHeader
- ChatInput
- ChatMessageActions
- ChatMessageEditor
- ChatMessage
- ChatTypingIndicator
- ChatWindowCsatPrompt
- ChatWindowQuote
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- ContentReview
- Countdown
- CriteriaDates
- CriteriaParticipants
- CriteriaTransports
- Date Field
- Datepicker
- Dropdown
- ElasticHeight
- ExpandableCard
- ExpandableText
- Filter
- FloatingLabelTextField
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- JourneyCard
- LayerHeader
- Layer
- Link
- Loader
- Number Field
- PageNavigation
- Pagination
- Paragraph
- Password
- Phone Field
- Popin
- Portal
- ProductThumbnail
- Promises
- Prose
- Radio Group
- RadioSelect
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- SparkleWrapper
- Spinner
- Switch
- Table
- Tabs
- Tag
- TextField
- Toggle
- Tooltip
- ValidationMessage
import { Scrollbar } from '../ui/Scrollbar';
export function ScrollbarDemo() {
return (
<div>
<Scrollbar
aria-label="Demo horizontal scroller"
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
aria-label="Dark horizontal scroller"
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
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The scrollable content |
timelineName | string | unique per instance | CSS timeline name for the scroll animation |
scrollbarWidth | string | '60%' | Width of the scrollbar thumb as a percentage |
variant | 'light' | 'dark' | 'light' | Color theme variant |
className | string | - | Additional classes for the container |
scrollerClassName | string | - | Additional classes for the scrollable area |
scrollbarClassName | string | - | Additional classes for the scrollbar track |
scrollerStyle | CSSProperties | - | Additional style of the scrollable area |
autoHide | boolean | false | Hides the track, and keeps the scrollable area out of the tab order and of the landmarks, while the content fits |
aria-label | string | 'Scrollable content' | Accessible label for the scrollable region |
aria-labelledby | string | - | Id of the element naming the scrollable region; wins over aria-label |
Custom Timeline Name
Each instance gets a unique timeline name by default, so several scrollbars can live on the same page. Set one yourself to reference it from your own CSS:
<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 customizablearia-labelrole="scrollbar"on the scrollbar track with orientation and value attributes- Keyboard navigation support with
tabIndex={0}on the scrollable area - The track points to the scrollable area (
aria-controls) and announces the scroll position inaria-valuenow, from 0 to 100 - With
autoHide, content that fits is neither a tab stop nor a landmark, and the track is hidden
Auto hide
autoHide measures the content with a ResizeObserver and only displays the track while it overflows:
<Scrollbar autoHide aria-labelledby="prices-title">
<table>{/* rows */}</table>
</Scrollbar>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:
- Container - Defines the timeline scope
- Scroller (
data-scroller) - The scrollable container that creates the scroll timeline - 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
scrollTimelineproperty with thexaxis:'--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.