A Sliding Nightmare: Understanding the Range Input


You may have already seen a bunch of tutorials on how to style the range input. While this is another article on that topic, it’s not about how to get any specific visual result. Instead, it dives into browser inconsistencies, detailing what each does to display that slider on the screen. Understanding this is important because it helps us have a clear idea about whether we can make our slider look and behave consistently across browsers and which styles are necessary to do so.

Looking inside a range input

Before anything else, we need to make sure the browser exposes the DOM inside the range input.

In Chrome, we bring up DevTools, go to Settings, Preferences, Elements and make sure the Show user agent shadow DOM option is enabled.

Sequence of Chrome screenshots illustrating the steps from above.

In Firefox, we go to about:config and make sure the devtools.inspector.showAllAnonymousContent flag is set to true.

Sequence of Firefox screenshots illustrating the steps from above.

For a very long time, I was convinced that Edge offers no way of seeing what’s inside such elements. But while messing with it, I discovered that where there’s a will and (and some dumb luck) there’s a way! We need to bring up DevTools, then go to the range input we want to inspect, right click it, select Inspect Element and bam, the DOM Explorer panel now shows the structure of our slider!

Sequence of Edge screenshots illustrating the steps from above.

Apparently, this is a bug. But it’s also immensely useful, so I’m not complaining.

The structure inside

Right from the start, we can see a source for potential problems: we have very different beasts inside for every browser.

In Chrome, at the top of the shadow DOM, we have a div we cannot access anymore. This used to be possible back when /deep/ was supported, but then the ability to pierce through the shadow barrier was deemed to be a bug, so what used to be a useful feature was dropped. Inside this div, we have another one for the track and, within the track div, we have a third div for the thumb. These last two are both clearly labeled with an id attribute, but another thing I find strange is that, while we can access the track with ::-webkit-slider-runnable-track and the thumb with ::-webkit-slider-thumb, only the track div has a pseudo attribute with this value.

Inner structure in Chrome.

In Firefox, we also see three div elements inside, only this time they’re not nested – all three of them are siblings. Furthermore, they’re just plain div elements, not labeled by any attribute, so we have no way of telling which is which component when looking at them for the first time. Fortunately, selecting them in the inspector highlights the corresponding component on the page and that’s how we can tell that the first is the track, the second is the progress and the third is the thumb.

Inner structure in Firefox.

We can access the track (first div) with ::-moz-range-track, the progress (second div) with ::-moz-range-progress and the thumb (last div) with ::-moz-range-thumb.

The structure in Edge is much more complex, which, to a certain extent, allows for a greater degree of control over styling the slider. However, we can only access the elements with -ms- prefixed IDs, which means there are also a lot of elements we cannot access, with baked in styles we’d often need to change, like the overflow: hidden on the elements between the actual input and its track or the transition on the thumb’s parent.

Inner structure in Edge.

Having a different structure and being unable to access all the elements inside in order to style everything as we wish means that achieving the same result in all browsers can be very difficult, if not even impossible, even if having to use a different pseudo-element for every browser helps with setting individual styles.

We should always aim to keep the individual styles to a minimum, but sometimes it’s just not possible, as setting the same style can produce very different results due to having different structures. For example, setting properties such as opacity or filter or even transform on the track would also affect the thumb in Chrome and Edge (where it’s a child/ descendant of the track), but not in Firefox (where it’s its sibling).

The most efficient way I’ve found to set common styles is by using a Sass mixin because the following won’t work:

input::-ms-track { /* common styles */ }

To make it work, we’d need to write it like this:

input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }

But that’s a lot of repetition and a maintainability nightmare. This is what makes the mixin solution the sanest option: we only have to write the common styles once so, if we decide to modify something in the common styles, then we only need to make that change in one place – in the mixin.

@mixin track() { /* common styles */ }

input {
&::-webkit-slider-runnable-track { @include track }
&::-moz-range-track { @include track }
&::-ms-track { @include track }

Note that I’m using Sass here, but you may use any other preprocessor. Whatever you prefer is good as long as it avoids repetition and makes the code easier to maintain.

Initial styles

Next, we take a look at some of the default styles the slider and its components come with in order to better understand which properties need to be set explicitly to avoid visual inconsistencies between browsers.

Just a warning in advance: things are messy and complicated. It’s not just that we have different defaults in different browsers, but also changing a property on one element may change another in an unexpected way (for example, when setting a background also changes the color and adds a border).

WebKit browsers and Edge (because, yes, Edge also applies a lot of WebKit prefixed stuff) also have two levels of defaults for certain properties (for example those related to dimensions, borders, and backgrounds), if we may call them that – before setting -webkit-appearance: none (without which the styles we set won’t work in these browsers) and after setting it. The focus is going to be however on the defaults after setting -webkit-appearance: none because, in WebKit browsers, we cannot style the range input without setting this and the whole reason we’re going through all of this is to understand how we can make our lives easier when styling sliders.

Note that setting -webkit-appearance: none on the range input and on the thumb (the track already has it set by default for some reason) causes the slider to completely disappear in both Chrome and Edge. Why that happens is something we’ll discuss a bit later in this article.

The actual range input element

The first property I’ve thought about checking, box-sizing, happens to have the same value in all browsers – content-box. We can see this by looking up the box-sizing property in the Computed tab in DevTools.

The box-sizing of the range input, comparative look at all three browsers (from top to bottom: Chrome, Firefox, Edge).

Sadly, that’s not an indication of what’s to come. This becomes obvious once we have a look at the properties that give us the element’s boxes – margin, border, padding, width, height.

By default, the margin is 2px in Chrome and Edge and 0 .7em in Firefox.

Before we move on, let’s see how we got the values above. The computed length values we get are always px values.

However, Chrome shows us how browser styles were set (the user agent stylesheet rule sets on a grey background). Sometimes the computed values we get weren’t explicitly set, so that’s no use, but in this particular case, we can see that the margin was indeed set as a px value.

Tracing browser styles in Chrome, the margin case.

Firefox also lets us trace the source of the browser styles in some cases, as shown in the screenshot below: