A designer opens a ticket that says "the page jumps when the modal opens". You open DevTools and find nothing: no animation, no layout thrash, no transform. The page moves about fifteen pixels to the right and then back again, and the only thing that changed is that the body stopped scrolling.
That is the scrollbar disappearing and taking its gutter with it. Tailwind CSS v4.3 shipped first-party scrollbar-width, scrollbar-color and scrollbar-gutter utilities in May 2026, and one of them fixes that bug in a single class. The other two finally let you delete the ::-webkit-scrollbar block that every project accumulates.
The three Tailwind scrollbar utilities#
Three CSS properties, three families of utility. Here is the whole surface area:
| Class | Generated CSS |
|---|---|
scrollbar-auto |
scrollbar-width: auto; |
scrollbar-thin |
scrollbar-width: thin; |
scrollbar-none |
scrollbar-width: none; |
scrollbar-thumb-slate-500 |
--tw-scrollbar-thumb: var(--color-slate-500); scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); |
scrollbar-track-slate-100 |
--tw-scrollbar-track: var(--color-slate-100); scrollbar-color: var(--tw-scrollbar-thumb) var(--tw-scrollbar-track); |
scrollbar-gutter-auto |
scrollbar-gutter: auto; |
scrollbar-gutter-stable |
scrollbar-gutter: stable; |
scrollbar-gutter-both |
scrollbar-gutter: stable both-edges; |
A typical scroll region uses three of them at once:
<div class="scrollbar-thin scrollbar-thumb-slate-500 scrollbar-track-slate-100 h-96 overflow-y-auto">
<!-- long list -->
</div>
Note the split in how the colour utilities compile. scrollbar-color is a single CSS property that takes two values, so Tailwind sets two custom properties and writes the shorthand from both. That is why scrollbar-thumb-* and scrollbar-track-* compose cleanly instead of overwriting each other — and it is also the source of the most common gotcha, which I get to below.
The colour utilities accept the full palette, the opacity modifier, arbitrary values and CSS variables:
<!-- Palette with an opacity modifier -->
<div class="scrollbar-thumb-slate-900/60 scrollbar-track-slate-900/10">…</div>
<!-- Arbitrary value -->
<div class="scrollbar-thumb-[#0f766e] scrollbar-track-[#ccfbf1]">…</div>
<!-- CSS variable -->
<div class="scrollbar-thumb-(--brand-thumb) scrollbar-track-(--brand-track)">…</div>
scrollbar-width does not get the same treatment. It takes the three browser keywords and nothing else — scrollbar-thin-[6px] is not a thing, and that is deliberate on the CSS side, not a gap in Tailwind.
Standard properties, not ::-webkit-scrollbar#
There have been two ways to style a scrollbar for about a decade, and they do not overlap.
::-webkit-scrollbar and its siblings (::-webkit-scrollbar-thumb, -track, -button, -corner) are pseudo-elements. They give you real CSS boxes, so you get border-radius, box-shadow, :hover on the thumb itself, margins on the track — and none of it has ever worked in Firefox.
scrollbar-width and scrollbar-color are standard properties. They give you far less: a width keyword, a thumb colour, a track colour. In exchange they work in Firefox, Chrome and Safari from one declaration.
Tailwind implements the standard properties. That is the right call, and it means moving off the pseudo-elements costs you specific things:
- Thumb
border-radius— gone. - A track with padding or an inset thumb — gone.
- Scrollbar buttons (the little arrows) — gone.
- A true hover state on the thumb — gone, with an important asterisk.
The asterisk: Tailwind does support hover:scrollbar-thumb-sky-500, but the variant is scoped to the element, not the thumb. It fires when the pointer is anywhere over the scroll container, which is a different effect from the thumb lighting up when you are about to grab it. It is useful — a scrollbar that fades in when you hover the sidebar is a good pattern — just do not expect it to reproduce the WebKit behaviour.
What you keep is consistency, and consistency is worth more than a rounded thumb that a third of your users never see.
scrollbar-gutter-stable and the layout shift fix#
This is the utility that earns the release.
scrollbar-gutter controls whether the browser reserves space for a classic (space-consuming) scrollbar before it needs one. Default is auto: no space until the content overflows, at which point the scrollbar appears and the content box gets narrower. Anything centred inside it moves.
<!-- Content shifts left the moment it overflows -->
<div class="h-64 overflow-y-auto">…</div>
<!-- Gutter reserved up front; nothing moves when content grows -->
<div class="scrollbar-gutter-stable h-64 overflow-y-auto">…</div>
The version that shows up in tickets is the modal. You lock body scroll by setting overflow: hidden on <html>, the document scrollbar vanishes, the viewport gets ~15px wider, and every centred element on the page slides right. Close the modal and it slides back. Users describe it as "flickering" and nobody looks at the scrollbar, because the scrollbar is not the thing they can see moving.
One class on the root element:
<html class="scrollbar-gutter-stable">
The gutter is now permanently reserved, so removing the scrollbar changes nothing. This costs you 15px of viewport width on pages that do not scroll, which is almost always the better trade.
scrollbar-gutter-both (stable both-edges) reserves matching space on the opposite edge too. Reach for it when the scroll container's contents are visually centred and an asymmetric gutter would make them look off-centre — a centred prose column in a scrollable panel is the usual case. For a left-aligned sidebar or table it is wasted space.
Theming the scrollbar with your design tokens#
Because the colour utilities read from --color-*, custom theme colours work with no extra wiring:
@import "tailwindcss";
@theme {
--color-surface-track: oklch(96% 0.01 250);
--color-surface-thumb: oklch(62% 0.03 250);
}
<div class="scrollbar-thumb-surface-thumb scrollbar-track-surface-track overflow-y-auto">…</div>
If you are already generating token ramps, the same approach in building design tokens with color-mix and OKLCH applies directly — a scrollbar thumb is just another token consumer.
For dark mode, use the variant rather than a second set of tokens:
<div class="scrollbar-thumb-slate-400 scrollbar-track-slate-100
dark:scrollbar-thumb-slate-500 dark:scrollbar-track-slate-800
overflow-y-auto">
…
</div>
That assumes dark: is wired to a class rather than prefers-color-scheme. If you have not set that up, CSS-first dark mode with @custom-variant covers the one-line version in Tailwind v4.
There is a cheaper option worth knowing: color-scheme: dark (Tailwind's scheme-dark) makes the browser render its default scrollbar in dark colours with no scrollbar-color at all. If you only want "the scrollbar should not be blinding white in dark mode", that is the whole fix, and it keeps the platform's native appearance.
Scoping to a single container#
scrollbar-color inherits. Set it on <html> and every nested scroll region in the app picks it up, including ones inside third-party widgets.
Most of the time that is not what you want. Put the utilities on the scroll container itself:
<aside class="scrollbar-thin scrollbar-thumb-slate-300 scrollbar-track-transparent
h-screen w-64 overflow-y-auto">
<!-- navigation -->
</aside>
<div class="scrollbar-thin scrollbar-thumb-slate-400 scrollbar-track-slate-100
max-h-[32rem] overflow-y-auto">
<table class="w-full">…</table>
</div>
scrollbar-width does not inherit, so a scrollbar-thin on <html> will not thin the sidebar's scrollbar. This asymmetry catches people: colours cascade, widths do not. If you want a consistent look across every scroll region, apply both explicitly to each one, or set the colour once on <html> and accept it everywhere.
Hiding the scrollbar responsibly#
scrollbar-none hides the bar while the element still scrolls. It is the honest replacement for the .no-scrollbar utility every codebase has, which was two vendor-prefixed blocks and an -ms-overflow-style declaration.
The legitimate uses are horizontal: a tab strip, a chip row, a card carousel. Every one of them needs a different affordance, because you have just removed the only visual signal that the region scrolls.
<div class="scrollbar-none snap-x snap-mandatory flex gap-4 overflow-x-auto">
<article class="snap-start shrink-0 basis-[85%]">…</article>
<article class="snap-start shrink-0 basis-[85%]">…</article>
<article class="snap-start shrink-0 basis-[85%]">…</article>
</div>
Two things make this defensible. The partially visible next card is the affordance — that basis-[85%] is doing accessibility work, not just looking nice. And keyboard scrolling still works, because the container is a scroll container regardless of whether the bar is painted; add tabindex="0" so keyboard users can actually focus it. The full pattern, including snap alignment and the arrow controls, is in scroll-snap carousels in Tailwind.
What is not defensible is scrollbar-none on a vertical list to make a design look cleaner. Nothing indicates there is more content, and nothing indicates where you are in it.
Contrast and touch targets#
A scrollbar thumb is a control. It is draggable, it conveys position, and WCAG 2.1's non-text contrast criterion (1.4.11) asks for 3:1 against adjacent colours for exactly that kind of element.
The pale-grey-on-white thumb that looks tasteful on a calibrated monitor in a dark office is frequently under 2:1, and it disappears entirely on a cheap laptop screen in daylight. Concrete pairings that hold up:
<!-- Light: slate-400 on slate-100 -->
<div class="scrollbar-thumb-slate-400 scrollbar-track-slate-100">…</div>
<!-- Dark: slate-500 on slate-800 -->
<div class="dark:scrollbar-thumb-slate-500 dark:scrollbar-track-slate-800">…</div>
Check the pair you actually ship rather than trusting the swatch names — any contrast checker will do, and the thumb-versus-track comparison is the one that matters, not thumb-versus-page.
scrollbar-thin has a separate problem on touch: it narrows a drag target that is already at the edge of the screen. Most touch devices use overlay scrollbars and ignore the width anyway, but on a hybrid device with a stylus or a trackpad it is a real hit area. Gate it on pointer type:
<div class="scrollbar-auto pointer-fine:scrollbar-thin overflow-y-auto">…</div>
The pointer and any-pointer variants article covers why pointer-fine is the right test here rather than a breakpoint — screen width tells you nothing about what the user is pointing with.
Migrating off the plugin to Tailwind's scrollbar utilities#
The before, in a project running tailwind-scrollbar plus a hand-written fallback:
// tailwind.config.js — or whatever survived your v4 upgrade
plugins: [require('tailwind-scrollbar')],
/* app.css — the block every project has */
.custom-scroll::-webkit-scrollbar { width: 8px; }
.custom-scroll::-webkit-scrollbar-track { background: #f1f5f9; }
.custom-scroll::-webkit-scrollbar-thumb {
background: #94a3b8;
border-radius: 4px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover { background: #64748b; }
/* Firefox ignores all of the above */
.custom-scroll { scrollbar-width: thin; scrollbar-color: #94a3b8 #f1f5f9; }
The after is one line of markup and no CSS:
<div class="scrollbar-thin scrollbar-thumb-slate-400 scrollbar-track-slate-100
hover:scrollbar-thumb-slate-500 overflow-y-auto">
…
</div>
Drop the plugin from package.json, delete the .custom-scroll block, and run a find-and-replace for scrollbar-thumb-slate-400 style classes if you were using the plugin's own naming (its classes share a prefix with the first-party ones, so check that you have not left both in the class list where they would fight). If you are doing this as part of a larger v3-to-v4 move, the Tailwind upgrade tool for Laravel projects handles the config migration around it.
What you lose in that diff is the 4px thumb radius and the true thumb hover. If a specific surface genuinely needs them, keep a narrow @utility for that one case and accept it is Chromium and Safari only:
@utility scroll-rounded {
&::-webkit-scrollbar-thumb {
border-radius: 9999px;
}
}
Registering it as a custom utility with the @utility directive keeps it in one reviewable place rather than scattered through component CSS, and makes it obvious to the next person that this is the engine-specific escape hatch.
Gotchas and Edge Cases#
Setting one colour transparents the other. scrollbar-color is one property taking two values. Write scrollbar-thumb-sky-700 alone and the track resolves to transparent, which on a light page reads as "the track disappeared". Always set both, even if one is explicitly scrollbar-track-transparent.
scrollbar-gutter only applies to scroll containers. Put scrollbar-gutter-stable on a div with no overflow and nothing happens. It looks like the utility is broken; the element simply is not a scroll container. Pair it with overflow-auto, overflow-y-auto or overflow-scroll.
macOS overlay scrollbars hide the effect. On a Mac with a trackpad, scrollbars are overlays that consume no layout space, so scrollbar-gutter-stable reserves a gutter you can see in DevTools but the shift you were fixing never occurred in the first place. Plug in a mouse (System Settings → Appearance → Show scroll bars → Always) or test on Windows before concluding the class does nothing.
scrollbar-color cascades, scrollbar-width does not. Component-level colour overrides have to be explicit if you set anything on <html>. Width has to be set per container regardless.
Windows high-contrast mode overrides all of it. Forced-colors mode replaces your scrollbar colours with system ones. That is correct behaviour and you should not fight it — it is one more reason not to build a UI where the scrollbar's exact colour carries meaning.
Wrapping Up#
Add scrollbar-gutter-stable to your <html> element this afternoon. It is one class, it removes a whole category of "the page flickers" tickets, and it costs 15px of width you were not using. Then delete tailwind-scrollbar and the ::-webkit-scrollbar block, and check your thumb colour against its track while you are in the file.
If you are auditing scroll regions generally, scroll-snap carousels is the natural companion — hidden scrollbars and snap points are the same decision made twice. And wrap-anywhere versus wrap-break-word for long URLs covers the other reason a table starts scrolling sideways when you did not ask it to.
FAQ#
How do I style a scrollbar with Tailwind CSS?
Use the first-party utilities added in Tailwind v4.3: scrollbar-thin for width, scrollbar-thumb-* and scrollbar-track-* for colour, and scrollbar-gutter-* for reserved space. Apply them to the scroll container itself alongside an overflow utility, for example scrollbar-thin scrollbar-thumb-slate-400 scrollbar-track-slate-100 overflow-y-auto. No plugin and no custom CSS is required.
What is scrollbar-gutter and when should I use stable?
scrollbar-gutter controls whether the browser reserves space for a scrollbar before one is needed. Use scrollbar-gutter-stable whenever content in a scroll container can grow past its height, or on <html> when you lock body scroll for modals — it stops the layout shifting as the scrollbar appears and disappears. Use scrollbar-gutter-both only when the container's contents are visually centred and an asymmetric gutter would look wrong.
How do I make a thin scrollbar in Tailwind?
Add the scrollbar-thin class to the scroll container. It maps to scrollbar-width: thin, one of only three keywords the property accepts alongside auto and none. You cannot specify a pixel width — thin means whatever the browser decides for the platform, which is typically around 8 to 10 pixels on desktop and nothing at all on touch devices that use overlay scrollbars.
Do I still need the tailwind-scrollbar plugin?
For width, thumb colour and track colour, no — those are all first-party from Tailwind v4.3 onwards, so the plugin can come out of package.json. You would only keep a plugin or hand-written CSS if you need WebKit-only details the standard properties do not expose, such as a rounded thumb, a padded track or scrollbar buttons. Those work in Chromium and Safari only, so weigh whether they are worth the inconsistency.
Why does my page shift when a scrollbar appears?
Because a classic scrollbar consumes layout width. When content grows past the viewport the scrollbar appears, the content box narrows by roughly 15 pixels, and anything centred inside it moves. The same thing happens in reverse when you set overflow: hidden on <html> to lock scrolling behind a modal. Adding scrollbar-gutter-stable to the root element reserves the space permanently so nothing moves either way.
Can I hide the scrollbar but keep scrolling in Tailwind?
Yes — scrollbar-none sets scrollbar-width: none, which hides the bar while the element still scrolls by wheel, touch and keyboard. Use it sparingly, because you have removed the only visual cue that the region is scrollable. Horizontal carousels and tab strips are reasonable cases as long as a partially visible next item signals there is more; a vertical list with a hidden scrollbar leaves the user no indication of position or length.