Tailwind CSS 61 🎨 Custom Scrollbar Utilities and CSS Scroll-Driven Animations Integration
Scrollbars are the most overlooked part of a web interface until they are wrong. A bright default scrollbar on a dark theme ruins the design. A scroll container without a visible scrollbar leaves the user unsure whether the content is scrollable. Tailwind provides utilities for styling the scrollbar itself, and modern CSS provides scroll-driven animations that tie an element’s animation to the scroll progress of its container. Together, they let a scroll area look and behave exactly as the design intends.
Key point: scrollbar-width and scrollbar-color are the standard properties, supported in Firefox and Chromium. The ::-webkit-scrollbar pseudo-elements are the WebKit-only alternative. Tailwind’s scrollbar-* utilities and arbitrary variants cover both, and the developer chooses based on the target browsers.
Why custom scrollbars and scroll-driven animations matter
The visual inconsistency problem. A default scrollbar is rendered by the operating system. It has the OS theme, the OS width, and the OS behavior. A dark theme with a bright scrollbar, a narrow sidebar with a wide scrollbar, and a rounded container with a square scrollbar all look wrong. The custom scrollbar is the fix.
The discoverability problem. A scroll container with a hidden scrollbar does not signal that there is more content. The user must guess that the container scrolls. A styled scrollbar, or a scroll indicator, restores the signal.
The scroll-driven animation problem. Parallax effects, progress bars, and reveal-on-scroll animations have traditionally required JavaScript. The animation-timeline: scroll() and view() properties bind a CSS animation to the scroll progress of a container or the visibility of an element, with no JavaScript and no event listener.
The performance problem. A JavaScript scroll listener fires on every scroll frame and runs the layout, the paint, and the handler. A CSS scroll-driven animation runs on the compositor, off the main thread, and does not fire events. The CSS approach is the modern replacement.
The progressive enhancement problem. The scrollbar-width property and the animation-timeline property are not supported in every browser. The @supports block provides the fallback, and the JavaScript listener is the fallback for the older browsers.
The accessibility problem. A custom scrollbar must remain visible for the users who rely on it. A hidden scrollbar and a scroll-driven animation that hides content are accessibility failures. The design must respect the prefers-reduced-motion and the forced-colors settings.
The design-system problem. The scrollbar is part of the design system. The width, the color, the radius, and the hover state are the tokens. Tailwind’s utilities and the design tokens together make the scrollbar consistent across the components.
a. The custom scrollbar utilities
The scrollbar styling uses two standards-based properties and a set of WebKit pseudo-elements. Tailwind exposes the standards-based properties as utilities and the pseudo-elements through arbitrary variants.
The scrollbar-width property
The scrollbar-width property sets the width of the scrollbar. The values are auto, thin, and none.
<div class="overflow-y-auto [scrollbar-width:thin]">
...
</div>
The thin value is the narrow scrollbar. The none value hides the scrollbar while keeping the scroll functionality.
The scrollbar-color property
The scrollbar-color property sets the thumb and the track colors. The two values are the thumb and the track.
<div class="overflow-y-auto [scrollbar-color:theme(colors.slate.400)_theme(colors.slate.100)]">
...
</div>
The first value is the thumb, and the second is the track. The theme() function reads the Tailwind color tokens.
The WebKit pseudo-elements
The ::-webkit-scrollbar pseudo-elements provide the fine-grained control in the Chromium and the Safari browsers.
.scrollbar-custom::-webkit-scrollbar {
width: 8px;
}
.scrollbar-custom::-webkit-scrollbar-track {
background: theme('colors.slate.100');
}
.scrollbar-custom::-webkit-scrollbar-thumb {
background: theme('colors.slate.400');
border-radius: 4px;
}
.scrollbar-custom::-webkit-scrollbar-thumb:hover {
background: theme('colors.slate.500');
}
The Tailwind arbitrary variants
The pseudo-elements are targetable through the arbitrary variants.
<div class="
overflow-y-auto
[&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-track]:bg-slate-100
[&::-webkit-scrollbar-thumb]:bg-slate-400
[&::-webkit-scrollbar-thumb]:rounded-full
[&::-webkit-scrollbar-thumb:hover]:bg-slate-500
">
...
</div>
The &::-webkit-scrollbar form is the arbitrary variant. The & is the current element, and the ::-webkit-scrollbar is the pseudo-element.
The plugin approach
A Tailwind plugin can package the scrollbar styles into reusable utilities.
// tailwind.config.js
const plugin = require('tailwindcss/plugin');
module.exports = {
plugins: [
plugin(({ addUtilities }) => {
addUtilities({
'.scrollbar-thin': {
'scrollbar-width': 'thin',
'&::-webkit-scrollbar': { width: '8px' },
},
'.scrollbar-thumb-slate': {
'&::-webkit-scrollbar-thumb': {
'background-color': 'rgb(148 163 184)',
'border-radius': '9999px',
},
},
});
}),
],
};
The plugin registers the utilities, and the components use the class names.
The scrollbar-gutter property
The scrollbar-gutter property reserves the space for the scrollbar, so the layout does not shift when the content overflows.
.scroll-gutter {
scrollbar-gutter: stable;
}
The stable value reserves the space on both edges. The both-edges value reserves it on both, and the stable is the common case.
The scrollbar styling table
| Property | Values | Browser |
|---|---|---|
scrollbar-width | auto, thin, none | Firefox, Chromium |
scrollbar-color | thumb, track | Firefox, Chromium |
::-webkit-scrollbar | — | Chromium, Safari |
scrollbar-gutter | auto, stable, both-edges | Chromium, Firefox |
The scrollbar accessibility
| Setting | Effect |
|---|---|
prefers-reduced-motion | The user prefers less motion |
forced-colors | The OS high-contrast mode |
scrollbar-width: none | The scrollbar is hidden |
scrollbar-color | The thumb and the track |
The scrollbar-width: none removes the visual indicator, and the scroll remains functional. The pattern is used with a custom scroll indicator, and it should not be the only signal.
b. The scroll-driven animations
The animation-timeline property binds a CSS animation to a scroll timeline. The two timelines are scroll() and view().
The scroll timeline
The scroll() timeline is the progress of a scroll container. The 0% is the top, and the 100% is the bottom.
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.progress-bar {
animation: grow linear;
animation-timeline: scroll(root);
}
The scroll(root) is the root scroller. The scroll(nearest) is the nearest scrollable ancestor. The scroll(self) is the element itself.
The view timeline
The view() timeline is the progress of an element through the viewport. The 0% is when the element enters, and the 100% is when it leaves.
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.reveal {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% cover 40%;
}
The animation-range controls the portion of the timeline. The entry is when the element enters the viewport, and the cover is when it fully covers.
The animation-range
The animation-range property sets the start and the end of the animation on the timeline.
| Value | Meaning |
|---|---|
entry 0% | The element enters the viewport |
entry 100% | The element fully enters |
cover 0% | The element starts to cover |
cover 100% | The element fully covers |
exit 0% | The element starts to leave |
exit 100% | The element fully leaves |
The Tailwind arbitrary values
Tailwind does not have the built-in utilities for the scroll-driven animations. The arbitrary properties are the mechanism.
<div class="
[animation-timeline:scroll(root)]
[animation-name:grow]
[animation-duration:auto]
[animation-timing-function:linear]
">
...
</div>
The scroll-driven utilities pattern
A Tailwind plugin can register the common scroll-driven utilities.
plugin(({ addUtilities }) => {
addUtilities({
'.timeline-scroll': {
'animation-timeline': 'scroll(root)',
'animation-duration': 'auto',
'animation-timing-function': 'linear',
},
'.timeline-view': {
'animation-timeline': 'view()',
'animation-duration': 'auto',
'animation-timing-function': 'linear',
},
});
});
The progress bar
The progress bar is the canonical example. The width of the bar is tied to the scroll progress.
<div class="fixed top-0 left-0 h-1 w-full bg-slate-200">
<div class="h-full bg-blue-600 origin-left [animation:grow_linear] [animation-timeline:scroll(root)]">
</div>
</div>
<style>
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
</style>
The bar grows as the user scrolls the page.
The parallax
The scroll() timeline with the transform: translateY() produces the parallax.
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-50%); }
}
.hero-image {
animation: parallax linear;
animation-timeline: scroll(root);
}
The reveal on scroll
The view() timeline with the opacity produces the reveal.
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: reveal linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
The scroll-driven table
| Timeline | Trigger |
|---|---|
scroll(root) | The root scroll progress |
scroll(nearest) | The nearest scroll ancestor |
scroll(self) | The element’s own scroll |
view() | The element’s viewport progress |
The browser support
The animation-timeline is supported in the Chromium browsers and the Safari browsers. The Firefox support is behind a flag. The fallback is the JavaScript scroll listener.
@supports (animation-timeline: scroll()) {
.progress-bar {
animation: grow linear;
animation-timeline: scroll(root);
}
}
The scroll-driven anti-patterns
| Anti-pattern | Problem |
|---|---|
| The animation without the timeline | The animation does not run |
The animation-duration not set to auto | The animation uses the duration, not the timeline |
The animation-timing-function not linear | The timeline progress is not linear |
| The JavaScript listener for the supported browsers | The compositor animation is faster |
| The reduced-motion not handled | The accessibility failure |
c. The integration
The two features compose. A custom scrollbar on a scroll container and a scroll-driven animation tied to the container’s scroll progress.
The styled scroll container with the progress bar
<div class="
h-96 overflow-y-auto
[scrollbar-width:thin]
[scrollbar-color:theme(colors.slate.400)_theme(colors.slate.100)]
[&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-thumb]:rounded-full
[&::-webkit-scrollbar-thumb]:bg-slate-400
">
<div class="h-[200vh]">Long content</div>
</div>
The container has the custom scrollbar, and the content is long enough to scroll.
The scroll-driven animation on the container
<div class="
h-96 overflow-y-auto
[scrollbar-width:thin]
">
<div class="sticky top-0 h-1 bg-slate-200">
<div class="h-full bg-blue-600 origin-left
[animation:grow_linear]
[animation-timeline:scroll(nearest)]">
</div>
</div>
<div class="h-[200vh]">Long content</div>
</div>
The progress bar is sticky at the top of the container, and its width is tied to the container’s scroll progress.
The reveal on scroll inside the container
<div class="h-96 overflow-y-auto [scrollbar-width:thin]">
<article class="
p-4
[animation:reveal_linear]
[animation-timeline:view()]
[animation-range:entry_0%_cover_40%]
">
Content
</article>
</div>
The article reveals as it enters the container’s viewport.
The reduced-motion handling
@media (prefers-reduced-motion: reduce) {
.scroll-animated {
animation: none;
}
}
The animation is disabled for the users who prefer reduced motion.
The @supports fallback
@supports (animation-timeline: scroll()) {
.scroll-animated {
animation: grow linear;
animation-timeline: scroll(root);
}
}
The @supports block applies the animation only when the browser supports the timeline.
The design token approach
The scrollbar tokens are part of the design system.
:root {
--scrollbar-width: thin;
--scrollbar-thumb: theme('colors.slate.400');
--scrollbar-track: theme('colors.slate.100');
}
.scrollbar-themed {
scrollbar-width: var(--scrollbar-width);
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
The tokens are the single source of truth, and the components reference them.
The integration table
| Feature | Property | Fallback |
|---|---|---|
| Scrollbar width | scrollbar-width | ::-webkit-scrollbar |
| Scrollbar color | scrollbar-color | ::-webkit-scrollbar-thumb |
| Scroll timeline | animation-timeline: scroll() | The JavaScript listener |
| View timeline | animation-timeline: view() | The IntersectionObserver |
| Reduced motion | prefers-reduced-motion | The default behavior |
Complete Example Session
<!-- ============================================
PART 1: THE BASIC SCROLLBAR STYLING
============================================ -->
<div class="h-96 overflow-y-auto [scrollbar-width:thin] [scrollbar-color:theme(colors.slate.400)_theme(colors.slate.100)]">
<div class="h-[200vh]">Content</div>
</div>
<!-- ============================================
PART 2: THE WEBKIT SCROLLBAR
============================================ -->
<div class="
h-96 overflow-y-auto
[&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-track]:bg-slate-100
[&::-webkit-scrollbar-thumb]:bg-slate-400
[&::-webkit-scrollbar-thumb]:rounded-full
[&::-webkit-scrollbar-thumb:hover]:bg-slate-500
">
<div class="h-[200vh]">Content</div>
</div>
<!-- ============================================
PART 3: THE HIDDEN SCROLLBAR
============================================ -->
<div class="h-96 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div class="h-[200vh]">Content</div>
</div>
<!-- ============================================
PART 4: THE SCROLLBAR GUTTER
============================================ -->
<div class="h-96 overflow-y-auto [scrollbar-gutter:stable]">
<div class="h-[50vh]">Short content</div>
<div class="h-[200vh]">Long content</div>
</div>
<!-- ============================================
PART 5: THE SCROLL PROGRESS BAR
============================================ -->
<div class="h-96 overflow-y-auto [scrollbar-width:thin]">
<div class="sticky top-0 h-1 bg-slate-200">
<div class="h-full origin-left bg-blue-600
[animation:grow_linear]
[animation-timeline:scroll(nearest)]">
</div>
</div>
<div class="h-[200vh] p-4">Content</div>
</div>
<style>
@keyframes grow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
</style>
<!-- ============================================
PART 6: THE PAGE PROGRESS BAR
============================================ -->
<div class="fixed left-0 top-0 z-50 h-1 w-full bg-slate-200">
<div class="h-full origin-left bg-blue-600
[animation:grow_linear]
[animation-timeline:scroll(root)]">
</div>
</div>
<!-- ============================================
PART 7: THE REVEAL ON SCROLL
============================================ -->
<article class="
p-6
[animation:reveal_linear]
[animation-timeline:view()]
[animation-range:entry_0%_cover_40%]
">
Content that reveals
</article>
<style>
@keyframes reveal {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<!-- ============================================
PART 8: THE PARALLAX
============================================ -->
<div class="relative h-screen overflow-hidden">
<div class="absolute inset-0
[animation:parallax_linear]
[animation-timeline:scroll(root)]
bg-cover bg-center"
style="background-image: url('/hero.jpg')">
</div>
</div>
<style>
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-30%); }
}
</style>
<!-- ============================================
PART 9: THE REDUCED MOTION
============================================ -->
<style>
@media (prefers-reduced-motion: reduce) {
.scroll-animated {
animation: none !important;
}
}
</style>
<!-- ============================================
PART 10: THE COMPLETE PATTERN
============================================ -->
<!--
1. Use scrollbar-width and scrollbar-color for the standards-based styling.
2. Use the ::-webkit-scrollbar pseudo-elements for the fine-grained control.
3. Use the arbitrary variants for the WebKit pseudo-elements.
4. Use scrollbar-gutter: stable to prevent the layout shift.
5. Use animation-timeline: scroll() for the container progress.
6. Use animation-timeline: view() for the reveal on scroll.
7. Use animation-range to control the portion of the timeline.
8. Use @supports for the fallback.
9. Use prefers-reduced-motion for the accessibility.
10. Use the design tokens for the consistency.
-->
The ten parts showed the basic scrollbar styling, the WebKit scrollbar, the hidden scrollbar, the scrollbar gutter, the scroll progress bar, the page progress bar, the reveal on scroll, the parallax, the reduced motion, and the complete pattern.
Quick Reference
The Scrollbar Properties
| Property | Values | Purpose |
|---|---|---|
scrollbar-width | auto, thin, none | The width |
scrollbar-color | thumb, track | The colors |
scrollbar-gutter | auto, stable, both-edges | The reserved space |
The WebKit Pseudo-Elements
| Pseudo-element | Purpose |
|---|---|
::-webkit-scrollbar | The scrollbar itself |
::-webkit-scrollbar-track | The track |
::-webkit-scrollbar-thumb | The thumb |
::-webkit-scrollbar-corner | The corner |
::-webkit-scrollbar-button | The buttons |
The Scroll-Driven Timelines
| Timeline | Trigger |
|---|---|
scroll(root) | The root scroll progress |
scroll(nearest) | The nearest scroll ancestor |
scroll(self) | The element’s own scroll |
view() | The element’s viewport progress |
The Animation Range
| Value | Meaning |
|---|---|
entry 0% | The element enters |
entry 100% | The element fully enters |
cover 0% | The element starts to cover |
cover 100% | The element fully covers |
exit 0% | The element starts to leave |
exit 100% | The element fully leaves |
The Browser Support
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
scrollbar-width | 121+ | 64+ | 18.2+ |
scrollbar-color | 121+ | 64+ | 18.2+ |
::-webkit-scrollbar | Yes | No | Yes |
animation-timeline | 115+ | Flag | 26+ |
Best Practices
✅ Do This:
<!-- Use the standards-based properties. -->
<div class="[scrollbar-width:thin] [scrollbar-color:theme(colors.slate.400)_theme(colors.slate.100)]"> <!-- ✅ -->
<!-- Use the WebKit pseudo-elements for the fine-grained control. -->
<div class="[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full"> <!-- ✅ -->
<!-- Use scrollbar-gutter to prevent the layout shift. -->
<div class="[scrollbar-gutter:stable]"> <!-- ✅ -->
<!-- Use animation-timeline: scroll() for the progress bar. -->
<div class="[animation-timeline:scroll(root)]"> <!-- ✅ -->
<!-- Use animation-timeline: view() for the reveal. -->
<div class="[animation-timeline:view()]"> <!-- ✅ -->
<!-- Use animation-range to control the portion. -->
<div class="[animation-range:entry_0%_cover_40%]"> <!-- ✅ -->
<!-- Use @supports for the fallback. -->
@supports (animation-timeline: scroll()) { ... } <!-- ✅ -->
<!-- Use prefers-reduced-motion. -->
@media (prefers-reduced-motion: reduce) { animation: none; } <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't rely only on the WebKit pseudo-elements. -->
<div class="[&::-webkit-scrollbar]:w-2"> <!-- ⚠️ Firefox ignores it -->
<!-- Don't hide the scrollbar without an alternative. -->
<div class="[scrollbar-width:none]"> <!-- ❌ The user does not know it scrolls -->
<!-- Don't forget the animation-duration: auto. -->
<div class="[animation-timeline:scroll(root)] [animation-duration:1s]"> <!-- ❌ The duration wins -->
<!-- Don't forget the animation-timing-function: linear. -->
<div class="[animation-timeline:scroll(root)] [animation-timing-function:ease]"> <!-- ❌ The timing distorts -->
<!-- Don't skip the reduced-motion handling. -->
<!-- The accessibility failure. --> <!-- ❌ -->
<!-- Don't use the JavaScript listener when the timeline works. -->
window.addEventListener('scroll', ...); <!-- ❌ The compositor is faster -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Scrollbar not styled in Firefox | Only WebKit pseudo-elements used | Add scrollbar-width and scrollbar-color |
| Layout shift on overflow | No scrollbar-gutter | Add [scrollbar-gutter:stable] |
| Animation does not run | No animation-timeline | Add the timeline |
| Animation uses the duration | animation-duration not auto | Set animation-duration: auto |
| Animation is not linear | The timing function | Set animation-timing-function: linear |
| Reveal does not fire | No animation-range | Add the range |
| Accessibility failure | No reduced-motion handling | Add the media query |
Real-World Examples
1. Basic Scrollbar
<div class="[scrollbar-width:thin] [scrollbar-color:theme(colors.slate.400)_theme(colors.slate.100)]">
2. WebKit Scrollbar
<div class="[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full">
3. Hidden Scrollbar
<div class="[scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
4. Scrollbar Gutter
<div class="[scrollbar-gutter:stable]">
5. Progress Bar
<div class="[animation-timeline:scroll(root)] [animation-name:grow]">
6. Reveal on Scroll
<article class="[animation-timeline:view()] [animation-range:entry_0%_cover_40%]">
7. Parallax
@keyframes parallax { from { transform: translateY(0); } to { transform: translateY(-30%); } }
8. Reduced Motion
@media (prefers-reduced-motion: reduce) { .scroll-animated { animation: none; } }
9. Supports Fallback
@supports (animation-timeline: scroll()) { ... }
10. The Complete Pattern
<div class="h-96 overflow-y-auto [scrollbar-width:thin] [&::-webkit-scrollbar]:w-2">
<div class="sticky top-0 h-1 bg-slate-200">
<div class="h-full bg-blue-600 [animation:grow_linear] [animation-timeline:scroll(nearest)]"></div>
</div>
</div>
Visual
The Scrollbar Anatomy
┌────────────────────────────────────────────────────────────┐
│ THE SCROLLBAR ANATOMY │
│ │
│ ┌──────────────────────────────────────┬───┐ │
│ │ Content │ ▲ │ │
│ │ │ │ │ ← thumb │
│ │ │ │ │ │
│ │ ├───┤ │
│ │ │ │ ← track │
│ │ │ │ │
│ │ │ │ │
│ │ │ ▼ │ │
│ └──────────────────────────────────────┴───┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ scrollbar-width: thin │ │
│ │ scrollbar-color: thumb track │ │
│ │ ::-webkit-scrollbar-thumb: the thumb │ │
│ │ ::-webkit-scrollbar-track: the track │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Scroll Timeline
┌────────────────────────────────────────────────────────────┐
│ THE SCROLL TIMELINE │
│ │
│ animation-timeline: scroll(root) │
│ │
│ Top of the page → 0% │
│ Middle of the page → 50% │
│ Bottom of the page → 100% │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @keyframes grow { │ │
│ │ from { transform: scaleX(0); } → 0% │ │
│ │ to { transform: scaleX(1); } → 100% │ │
│ │ } │ │
│ │ │ │
│ │ .progress-bar { │ │
│ │ animation: grow linear; │ │
│ │ animation-timeline: scroll(root); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The bar grows as the user scrolls. │
└────────────────────────────────────────────────────────────┘
The View Timeline
┌────────────────────────────────────────────────────────────┐
│ THE VIEW TIMELINE │
│ │
│ animation-timeline: view() │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ entry 0% → the element enters the viewport │ │
│ │ entry 100% → the element fully enters │ │
│ │ cover 0% → the element starts to cover │ │
│ │ cover 100% → the element fully covers │ │
│ │ exit 0% → the element starts to leave │ │
│ │ exit 100% → the element fully leaves │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ animation-range: entry 0% cover 40% │
│ → the animation runs from the entry to the 40% cover. │
└────────────────────────────────────────────────────────────┘
The Progress Bar
┌────────────────────────────────────────────────────────────┐
│ THE PROGRESS BAR │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ <div class="fixed top-0 h-1 w-full bg-slate-200"> │ │
│ │ <div class="h-full bg-blue-600 │ │
│ │ origin-left │ │
│ │ [animation:grow_linear] │ │
│ │ [animation-timeline:scroll(root)]"> │ │
│ │ </div> │ │
│ │ </div> │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ At the top: ░░░░░░░░░░░░░░░░░░░░ (scaleX 0) │
│ At the middle: ██████████░░░░░░░░░░ (scaleX 0.5) │
│ At the bottom: ████████████████████ (scaleX 1) │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
scrollbar-width | The scrollbar width |
scrollbar-color | The thumb and track colors |
::-webkit-scrollbar | The WebKit pseudo-element |
scrollbar-gutter | The reserved space |
animation-timeline: scroll() | The scroll progress |
animation-timeline: view() | The viewport progress |
animation-range | The portion of the timeline |
prefers-reduced-motion | The accessibility setting |
@supports | The fallback |
Key takeaways:
- The scrollbar styling uses the standards-based properties and the WebKit pseudo-elements. The
scrollbar-widthandscrollbar-colorproperties work in Firefox and Chromium. The::-webkit-scrollbarpseudo-elements provide the fine-grained control in the Chromium and Safari browsers. The Tailwind arbitrary variants target the pseudo-elements. - The
scrollbar-gutterproperty reserves the space. Thestablevalue prevents the layout shift when the content overflows. The property is the fix for the page that jumps when the scrollbar appears. - The
animation-timeline: scroll()binds the animation to the scroll progress. Thescroll(root)is the page scroll, thescroll(nearest)is the nearest scroll ancestor, and thescroll(self)is the element’s own scroll. - The
animation-timeline: view()binds the animation to the viewport progress. Theanimation-rangecontrols the portion of the timeline. Theentry, thecover, and theexitare the segments. - The
animation-durationmust beauto. The timeline controls the progress, and the duration is ignored. Theanimation-timing-functionmust belinearfor the progress to be linear. - The
@supportsblock is the fallback. Theanimation-timelineis not supported in every browser. The@supportsblock applies the animation only when the browser supports the timeline. - The
prefers-reduced-motionis the accessibility. The animation is disabled for the users who prefer reduced motion. The media query is the standard. - The design tokens make the scrollbar consistent. The width, the color, and the radius are the tokens. The components reference the tokens, and the design system is the single source of truth.
Remember: The custom scrollbars and the scroll-driven animations are the modern CSS features that make the scrollable UI look and behave as the design intends. The scrollbar-width and the scrollbar-color properties are the standards-based styling, and the ::-webkit-scrollbar pseudo-elements are the WebKit control. The animation-timeline: scroll() and the animation-timeline: view() bind the CSS animations to the scroll progress, with no JavaScript and no event listener. The animation-range, the @supports, and the prefers-reduced-motion handle the portion, the fallback, and the accessibility. The result is a scroll area that looks consistent across the browsers, that signals the scrollability, and that animates the content as the user scrolls.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!