| |

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

PropertyValuesBrowser
scrollbar-widthauto, thin, noneFirefox, Chromium
scrollbar-colorthumb, trackFirefox, Chromium
::-webkit-scrollbar—Chromium, Safari
scrollbar-gutterauto, stable, both-edgesChromium, Firefox

The scrollbar accessibility

SettingEffect
prefers-reduced-motionThe user prefers less motion
forced-colorsThe OS high-contrast mode
scrollbar-width: noneThe scrollbar is hidden
scrollbar-colorThe 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.

ValueMeaning
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

TimelineTrigger
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-patternProblem
The animation without the timelineThe animation does not run
The animation-duration not set to autoThe animation uses the duration, not the timeline
The animation-timing-function not linearThe timeline progress is not linear
The JavaScript listener for the supported browsersThe compositor animation is faster
The reduced-motion not handledThe 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

FeaturePropertyFallback
Scrollbar widthscrollbar-width::-webkit-scrollbar
Scrollbar colorscrollbar-color::-webkit-scrollbar-thumb
Scroll timelineanimation-timeline: scroll()The JavaScript listener
View timelineanimation-timeline: view()The IntersectionObserver
Reduced motionprefers-reduced-motionThe 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

PropertyValuesPurpose
scrollbar-widthauto, thin, noneThe width
scrollbar-colorthumb, trackThe colors
scrollbar-gutterauto, stable, both-edgesThe reserved space

The WebKit Pseudo-Elements

Pseudo-elementPurpose
::-webkit-scrollbarThe scrollbar itself
::-webkit-scrollbar-trackThe track
::-webkit-scrollbar-thumbThe thumb
::-webkit-scrollbar-cornerThe corner
::-webkit-scrollbar-buttonThe buttons

The Scroll-Driven Timelines

TimelineTrigger
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

ValueMeaning
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

FeatureChromeFirefoxSafari
scrollbar-width121+64+18.2+
scrollbar-color121+64+18.2+
::-webkit-scrollbarYesNoYes
animation-timeline115+Flag26+

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

PitfallWhy It HappensFix
Scrollbar not styled in FirefoxOnly WebKit pseudo-elements usedAdd scrollbar-width and scrollbar-color
Layout shift on overflowNo scrollbar-gutterAdd [scrollbar-gutter:stable]
Animation does not runNo animation-timelineAdd the timeline
Animation uses the durationanimation-duration not autoSet animation-duration: auto
Animation is not linearThe timing functionSet animation-timing-function: linear
Reveal does not fireNo animation-rangeAdd the range
Accessibility failureNo reduced-motion handlingAdd 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

ItemValue
scrollbar-widthThe scrollbar width
scrollbar-colorThe thumb and track colors
::-webkit-scrollbarThe WebKit pseudo-element
scrollbar-gutterThe reserved space
animation-timeline: scroll()The scroll progress
animation-timeline: view()The viewport progress
animation-rangeThe portion of the timeline
prefers-reduced-motionThe accessibility setting
@supportsThe fallback

Key takeaways:

  • The scrollbar styling uses the standards-based properties and the WebKit pseudo-elements. The scrollbar-width and scrollbar-color properties work in Firefox and Chromium. The ::-webkit-scrollbar pseudo-elements provide the fine-grained control in the Chromium and Safari browsers. The Tailwind arbitrary variants target the pseudo-elements.
  • The scrollbar-gutter property reserves the space. The stable value 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. The scroll(root) is the page scroll, the scroll(nearest) is the nearest scroll ancestor, and the scroll(self) is the element’s own scroll.
  • The animation-timeline: view() binds the animation to the viewport progress. The animation-range controls the portion of the timeline. The entry, the cover, and the exit are the segments.
  • The animation-duration must be auto. The timeline controls the progress, and the duration is ignored. The animation-timing-function must be linear for the progress to be linear.
  • The @supports block is the fallback. The animation-timeline is not supported in every browser. The @supports block applies the animation only when the browser supports the timeline.
  • The prefers-reduced-motion is 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!