Skip to main content

How to Style a Custom Scrollbar in CSS Without Breaking Firefox

Style a custom scrollbar in CSS that works across Chrome, Safari, Edge, and Firefox — WebKit pseudo-elements, scrollbar-color, cross-browser fallbacks.

Published By Li Lei
#css #scrollbar #frontend #browser-compat

How to Style a Custom Scrollbar in CSS Without Breaking Firefox

A custom scrollbar is one of those touches that makes a dark dashboard feel finished. The chunky gray operating-system bar sitting next to your carefully tuned palette is the visual equivalent of leaving the price tag on. So you reach for CSS, style it, ship it — and then a Firefox user sends a screenshot where the scrollbar is the default again. The reason is simple and frustrating: scrollbar styling lives in two separate worlds, and most tutorials only show you one of them.

This guide walks through both, why they exist, how to make a custom scrollbar that survives across browsers, and where to stop before you hurt the people actually trying to scroll.

Two worlds: WebKit pseudo-elements and the standard properties

Here is the single fact that explains every "it works on my machine" scrollbar bug: WebKit-based browsers use the ::-webkit-scrollbar pseudo-element family, while Firefox uses the standard scrollbar-color and scrollbar-width properties. They do not overlap. A rule written for one is invisible to the other.

Chrome, Safari, Edge, and Opera all run on WebKit/Blink, and they expose a small family of pseudo-elements:

  • ::-webkit-scrollbar — the whole bar (this is where you set width or height)
  • ::-webkit-scrollbar-track — the groove behind the thumb
  • ::-webkit-scrollbar-thumb — the draggable handle
  • ::-webkit-scrollbar-thumb:hover — the handle on hover
  • ::-webkit-scrollbar-button — the little arrow caps at each end

That gives you pixel-level control: exact widths, corner radii, gradients, the works.

Firefox went a different direction. It implements the W3C CSS Scrollbars spec, which is deliberately small. You get two properties and nothing more:

  • scrollbar-width — accepts only auto, thin, or none (no pixel value)
  • scrollbar-color — takes exactly two colors, thumb first then track

There is no Firefox equivalent for a track radius, a thumb radius, or those end buttons. That is on purpose. The narrow spec keeps scrollbars consistent with the operating system and avoids accessibility regressions from designers who get carried away.

Why you have to write both

If you only ship the ::-webkit-scrollbar block, every Firefox visitor falls back to the default bar. If you only ship scrollbar-color, every Chrome and Safari visitor gets a generic thin bar with no radius and no hover state. Neither half is enough on its own, and there is no shorthand that covers both.

So a correct custom scrollbar is always a combined block. Put the standard properties first so a Firefox-focused reviewer sees the relevant lines up top and so it reads as progressive enhancement — the cross-browser baseline, then the WebKit refinement:

/* Standard — Firefox */
.custom-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: #6366f1 #1e1e2e; /* thumb, then track */
}

/* WebKit — Chrome, Safari, Edge, Opera */
.custom-scrollbar::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
  background: #1e1e2e;
  border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
  background: #6366f1;
  border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: #818cf8;
}

That is a complete thin custom scrollbar: an 8px bar in WebKit, the closest thin keyword in Firefox, a matching indigo thumb and dark track in both, and a lighter hover state where the spec allows it. Notice the order inside scrollbar-color — thumb color comes first. Reverse them and you get a track-colored handle on a thumb-colored groove, which looks inverted. It is the single most common mistake I see in copied snippets.

Mapping pixels onto the Firefox keyword

The width mismatch trips people up the most. In WebKit you set width: 8px on the bar and you are done. In Firefox, scrollbar-width: 8px is invalid and silently dropped — the property does nothing, and your scrollbar quietly stays default.

The honest approach is to map your intended pixel width onto the nearest legal keyword: 0 becomes none, anything up to roughly 11px becomes thin, and larger values become auto. You will not get pixel parity in Firefox — the spec does not allow it — but you get the right size class and your colors carry through, which is the faithful maximum the standard permits.

When I first started styling scrollbars by hand, I burned an embarrassing amount of time staring at a Firefox window wondering why my 6px bar looked like the default. It was not a typo; the property simply does not accept pixels, and the browser was right to ignore it. Once I started thinking in "thin/auto/none" for Firefox and pixels only for WebKit, the cross-browser version stopped surprising me. That mental split is the whole game.

Hiding a scrollbar without breaking scroll

Sometimes the design calls for no visible bar at all — a horizontal card carousel, say, where the bar looks clumsy under the cards. You can hide it in both worlds while keeping the element scrollable:

.carousel { scrollbar-width: none; }              /* Firefox */
.carousel::-webkit-scrollbar { width: 0; height: 0; } /* WebKit */

The content still scrolls by wheel, drag, and trackpad. But be careful here: a hidden scrollbar removes a visible affordance — the cue that tells a user "there is more this way." That is fine for a decorative strip people will swipe instinctively. It is a usability problem on primary content, where someone genuinely needs to know they can scroll. Hide the bar for flourish, never for substance.

Scope it, and don't repaint the whole page

You can attach these rules to a universal selector (*) to restyle every scroll container on the page, or to a single class like .custom-scrollbar on just the element you want. The class form is almost always the safer default. A global *::-webkit-scrollbar rule also repaints scrollbars inside third-party widgets — an embedded chat box, a maps SDK, same-origin iframe content — that you never meant to touch. Scope to a class and you change exactly the containers you own.

This is also where contrast matters. A thumb that reads beautifully on your dark mockup can vanish on a light docs page. Test the actual background your scrollbar will sit against, not a convenient dark stage.

Skip the hand-tuning

You can absolutely write all of this by memory, but keeping the WebKit selector names straight, mapping pixels to Firefox keywords, and remembering the thumb-before-track color order gets old fast. The CSS Scrollbar Generator gives you sliders for width, radius, and colors with a live preview, then exports the combined WebKit-plus-Firefox block in the correct order with one copy. A designer can dial it in, copy the page URL — the full config rides in the query string — and hand engineering a link that opens the identical scrollbar.

If you are theming a whole component system, pair it with the Border Radius Generator so your scrollbar's corner radius matches the cards and panels it sits inside. Consistent radii are a small detail that quietly signals a deliberate design.

Style both worlds, lead with the standard properties, keep the affordance where it counts, and your custom scrollbar will look intentional in every browser instead of perfect in one.


Made by Toolora · Updated 2026-06-13