- Advanced Toast
- ArrowButton
- Arrows
- Avatar
- Backdrop
- Basic Toast
- Breadcrumb
- Button
- CardBackground
- Card
- Carousel
- ChatButton
- ChatInput
- ChatMessage
- ChatTypingIndicator
- ChatWindow
- CheckboxSelect
- Checkbox
- Checkboxes
- Chip
- ChoiceExpander
- Clickable
- Date Field
- Dropdown
- ElasticHeight
- ExpandableCard
- Filter
- Form Control
- Form Label
- Frame
- HamburgerIcon
- Heading
- Image
- Link
- Loader
- Number Field
- Pagination
- Password
- Phone Field
- Popin
- Portal
- Prose
- Radio Group
- Radio
- Range
- Scrollbar
- Select
- SidebarLayout
- SliderFrame
- Spinner
- Switch
- Tabs
- Tag
- TextField
- Tooltip
- ValidationMessage
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
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The scrollable content |
timelineName | string | '--scroller' | Unique 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 |
aria-label | string | '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 customizablearia-labelrole="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:
- 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.