Closed jgraham closed 6 months ago
CC @emilio
pew
I have a ton on this, scattered in way too many places, so just dumping a couple of things most people may not have seen already.
One, a couple of months ago, there was a CodePen slider challenge and I went through the tagged challenge pens, collected those that seemed finished and looked at what people were doing to achieve the desired designs.
Copy-pasting some numeric findings:
Related to value displays (most often as tooltips): 75%
of the sliders had their value displayed at least once (some twice)
Out of these:
67%
used a div
or span
for the value display13%
used a heading10%
used a p
8%
used a label
2.5%
used a number input
2.5%
used the title
attribute on the input
2.5%
used a pseudo on a wrapperNone of them used an output
, which I would have thought makes most sense to use here as it can be tied to the input
via the for
attribute. The only ones tying the value to the range input
were those displaying the value in a label
. I initially thought displaying the value should be left to an output
tied to the input
, but going through all those demos made me wonder whether it wouldn't be better to have a pseudo for this, like IE/ pre-Chromium Edge had ::-ms-tooltip
.
Related to ruler display: 32%
of sliders had a ruler
Out of these:
87.5%
used divs/ spans6.25%
used output
elements (none even tied to the range input
)6.25%
used ::before
& ::after
pseudos on the range input
👀 (this only works in Chrome and... uh, oh... I've done this too three quarters of a decade ago and it's such an awful and hacky and unreliable tactic to put the ruler numbers in one content
value, use a monospace font and position them with word-spacing
- I would have hoped nobody else would have this idea and if it was my 2015 demos that originated it, I am really sorry)None of these rulers were tied to the sliders unless you count the ::before
/::after
on range input
abomination. Again, while I would have thought datalist
was the way to go, as it can be tied to the range input
... none of the demos I saw was using that, which makes me wonder if the IE/ pre-Chromium Edge approach of putting everything but the kitchen sink in there wasn't the better way to go. I'd like to pick the mind of whoever came up with that and get the reasoning behind it.
Related to labels: 43%
of sliders had at least a label
Out of these:
p
elementslabel
elements (and out of these, all were tied to the range input
via the for
attribute 🥳)::before
/ ::after
pseudos on the range input
👀Related to how the actual sliders were made (excluding the components mentioned above):
20%
of sliders were made of divs, no range input
present (and only a single one of these went all the way when it came to replicating the native range input
functionality)12.5%
made of divs, invisible range input
present (unfortunately, not necessarily better than the previous category from a usability standpoint)4%
were SVG, invisible range input
present2%
were canvas, no range input
Related to usability:
72%
of the sliders had no :focus
styles29%
were not even focusable and did not allow changing the value via the keyboard27%
did not allow changing the value when clicking on the track7%
had performance issues due to replicating native functionality via JSMiscellaneous findings:
fill
component is a child of the track
, so it's best if the track
, fill
and thumb
are all siblings - for example, the sliders below could be easily coded if the 3 are siblings and the left edge of the border-box
of the fill
is always attached to the left edge of the content-box
of the track
while the right edge of the fill
is attached to the vertical midline of the thumb
Two, the below is a copy-paste of a case study that illustrates the problems when styling a simple flat slider:
Let's say we want to have the following slider.
Looks simple enough, doesn't it? What problems could we possibly run into with such a plain, common slider?
Well, brace yourselves...
Firstly, we start with a simple range input with the default 0
, 100
and 50
values for the min
, max
and value
. We'll be making a lot of assumptions that simplify things as we want to focus on the problems with structure/ range of motion that current implementations have.
<input type='range'/>
Let's say this range input will ocupy a 20em x 1.25em
box. In practice, this is probably going to be responsive, but here we're keeping everything as simple as possible.
$input-w: 20em;
$input-h: 1.25em;
[type='range'] {
width: $input-w;
height: $input-h;
color: #424e82
}
Next, a bit of custom styling for the thumb
, which includes setting dimensions, making it round in all browsers (only round by default in Firefox), giving it a background
and getting rid of the default border
it comes with in Firefox.
$thumb-d: $input-h;
@mixin thumb {
border: none;
width: $thumb-d;
height: $thumb-d;
border-radius: 50%;
background: currentColor
}
/* allow styling the slider in WebKit browsers */
[type='range'], ::-webkit-slider-thumb { appearance: none }
::-moz-range-thumb { @include thumb }
::-webkit-slider-thumb { @include thumb }
Note that we're using a mixin to avoid writing the same thing twice, as this selector:
::-webkit-slider-thumb,
::-moz-range-thumb { /* thumb styles */ }
... is invalid in WebKit browsers. It's valid in Firefox because in Firefox, the -webkit-
selector is an alias for the -moz-
one, due the widespread practice of only including -webkit-
prefixes. Pre-Chromium Edge used to do the same, btw.
All this stuff is detailed in my in-depth article on range inputs from over half a decade ago, though a lot of the default browser styles have changed still then.
Side note, wish accentColor
was a thing so we could set accent-color
on the body
and then use accentColor
for various components instead of setting color
on the actual input and then inheriting it from there.
Now it looks like we don't need to do much beside giving the track
a height
, a border-radius
that's half of it and a background
:
$track-h: 10px;
$track-r: .5*track-h;
@mixin track {
height: $track-h;
border-radius: $track-r;
background: #e4e7f5
}
::-moz-range-track { @include track }
::-webkit-slider-runnable-track { @include track }
And it looks the same in all current browsers so it seems like there's nothing else to do except add in the progress
. Except that's wrong. Our slider doesn't work as desired.
What this design (like most slider designs on the web, as this is by far the most common motion pattern encountered in designs) wants to achieve is the following:
That is: when the slider is at the minimum or at the maximum value, the outline of the round thumb
and the circle along which we have the track
rounding are concentric circles, their vertical midlines coincide.
What our code so far gives us is the following:
This is because, by default, the thumb
's border-box
moves within the limits of its parent's content-box
. Its parent is the track
in WebKit browsers and the actual slider in Firefox.
It only looks the same here as, in this very particular simple case, we don't have a margin
, a border
or a padding
on the track
. If we did, the Firefox result would be different from what we get in WebKit browsers.
But whether we're in this particular simple case or not, the different internal structure of the slider requires one approach for Firefox and another different approach for WebKit browsers.
That is... unless we done of the following:
thumb
a diameter equal to the track
's height
and then scale
it up by a factor that's $thumb-d/$track-r
- a problematic one as long as calc()
doesn't have unit division and a preprocessor cannot perform division between an em
value and a px
valuetrack
component and we just emulate the visual track using a linear-gradient()
rectangle and two radial-gradient()
discs - also problematic because of radial-gradient()
jaggedness (which requires a bit of edge blurring to fix, which is going to cause an awkward transition from the linear-gradient()
in the middle to the radial-gradient()
caps and every fix to that comes with its own set of problems) and because it fails when the visual track needs to have a box-shadow
or a gradient background
So we're pretty much stuck with different approaches for Firefox and WebKit browsers.
In Firefox, where the thumb
is a sibling of the track
and moves within the limits of the slider's content-box
, we can just shrink the track horizontally by an amount equal to the thumb
radius minus the track
radius at each end. This can be achieved by either seiing a margin
on it or by giving it a width
that's the $input-w
minus this amount - both approaches work to achieve the desired thumb
motion with respect to the track
in Firefox.
Or we could set both the margin
and the width
to later simplify our life when it comes to styling things in WebKit browsers.
[type='range'] {
--dif: calc(#{$thumb-r} - #{$track-r})
}
@mixin track() {
margin: var(--dif);
width: calc(#{$input-w} - 2*var(--dif);
/* same styles as before */
}
But in WebKit browsers, the thumb
is a child of the track
. It always moves within the limits of the track
's content-box
and there is nothing we can do about it.
Except for not setting the visual track styles on the actual track
component.
Which is hacky.
But so far, it has been the only way of achieving the result we're after here.
Since we have a different selector for the track
in WebKit browsers, we just don't set the track styles on it. Where do we set them then?
Well, we could set them on the actual slider.
Basically, it's like this... in Firefox, where the thumb
moves within the limit of the slider's content-box
, the slider is the big inviible box and the track
the one with the visual track styles. In WebKit browsers, where the thumb
moves within the limits of the track
's content-box
, it's the other way around. The track
is the big invisible box, while the actual slider gets the visual track styles set by the track
mixin.
However, this is also going to affect the slider's styles in Firefox, which is something we don't want. We could only set these styles in WebKit browsers by using @supports selector()
:
@supports selector(::-webkit-slider-runnable-track) {
[type='range'] { @include track }
}
This looks pretty unreliable because maybe one day Firefox starts passing the support test. Maybe this is better?
@supports not selector(::-moz-range-track) {
[type='range'] { @include track }
}
But isn't the use of @supports selector()
cutting off support for a lot of browsers?
There's still one more option. When looking at the internal structure of our slider in WebKit browser, we see that the track
is not directly inside the slider, but in a container inside the slider:
Now, if we are to use this to set the visual track styles on it, we need to override that one declaration:
-webkit-user-modify: read-only !important;
Which again feels hacky and yucky.
input[type='range']::-webkit-slider-container {
-webkit-user-modify: read-write !important;
@include track
}
Then, regardless of whether we've set the visual track styles on the actual slider or on this container component, we need to expand the actual track
component to cover the margin-box
on its ancestor that got the visual track styles.
::-webkit-slider-runnable-track { margin: calc(-1*var(--dif)) }
Note
I started my deep dive into prettifying native range inputs in January 2015. Both CSS and I got better in the meanwhile, but at the time, the only solution I saw in order to be able to reproduce certain designs was to use pseudo-elements on the track
and thumb
slider components.
This was an awful hack that only ever worked in Chromium browsers.
I first got it working via the /deep/
combinator, but then that got renamed to >>>
(so I had to change the code for close to a hundred sliders at the time) and then it got removed from the spec altogether (for reference) and Chrome binned it too before March 2015.
Then later that year, these selectors:
::-webkit-slider-runnable-track::before,
::-webkit-slider-runnable-track::after,
::-webkit-slider-thumb::before,
::-webkit-slider-thumb::after {}
started working, so I changed the code of what had become over a hundreds sliders once more. The selectors above working was short-lived too and at the end of 2015, I had a massive CodePen collection of styled range inputs, most of which were broken due to this and almost half of which remain broken to this day.
To make matters worse, this collection of styled range inputs is by far the most popular thing I have ever made.
If you've ever seen range inputs that were trying to style the ::before
and ::after
pseudos of the track
or thumb
and wondered what monster originated it... I'm afraid I'm that monster.
I have made the most popular demos in that collection private as they kept getting hearted, broken as they were. Plus people were forking them and creating their own demos based on them. And probably even more people just mindlessly copy-pasted the code.
This is one of the main reasons why I haven't gone for a full collection remake using one of the hacky WebKit tactics discussed above. Just like the earlier hacks, they could stop working and leave me with way too many broken demos once more.
Anyway, let's get back to styling our slider.
We still need to add the progress
. WebKit browsers don't have a dedicated element for this, so we need to rely on a bit of JS controling a gradient on the track. And by that, I don't mean the track
component in WebKit, I mean the visual track, which can be either the input
element or that container
... containing the actual track
component, which needs to be bigger that the visual track because its content-box
is the box that the thumb
's border-box
is constrained to.
The JS is pretty straightforward, we're just setting a --val
custom property to the current slider value.
addEventListener('input', e => {
let _t = e.target;
_t.style.setProperty('--v', +_t.value)
})
This custom property gets the default slider value 50
at the beginning:
<input type='range' style='--val: 50'/>
The position of the thumb
's vertical midline with respect to the visual track is also easily calculated.
At the slider's minimum value, the thumb
's vertical midline is one track radius $track-r
to the left of the visual track's left edge. At the slider's maximum value, the thumb
's vertical midline is one track radius $track-r
to the right of the visual track's right edge.
The distance between the position of the thumb
's vertical midline at the maximum value and its position of the minimum value is 100%
of the width of the visual track minus one track radius $track-r
at one end and another track radius $track-r
at the other end.
Since the slider's minimum value is 0
, the generic position --pos
of the thumb
's vertical midline is its position at the minimum value ($track-r
) plus how much the current value --val
represents of the maximum value (100
here) multiplied with the distance between its two extreme positions (at the maximum and at the minimum).
--pos: calc(#{$track-r} + var(--val)/100*(100% - 2*#{$track-r}));
This position is used to set a gradient on the visual track, but only for WebKit browsers:
@mixin track($progr: 0) {
/* same styles as before */
@if $progr != 0 {
background-image:
linear-gradient(90deg, currentColor var(--pos), tranparent 0)
}
}
::-moz-range-track { @include track }
input[type='range']::-webkit-slider-container {
-webkit-user-modify: read-write !important;
@include track(1)
}
Of course, we're not done. If JS is disabled, the visual progress is always at 50%, regardless of where we've dragged the slider to. So let's fix this with a --js
"flag": a custom property that's 0
if JS is disabled and 1
otherwise.
document.documentElement.classList.add('js')
.js { --js: 1 }
@mixin track($progr: 0) {
/* same styles as before */
@if $progr != 0 {
background-image:
linear-gradient(90deg, currentColor calc(var(--js)*var(--pos)), tranparent 0)
}
}
/* same as before */
This means we'll have no visual progress in the no JS case for WebKit browsers, but that's still better than having a broken one.
Now let's move on to Firefox, which has an actual progress
component. And you'd think it's as easy as giving it a background
and maybe a height
and a border-radius
... but it isn't!
This is what happens:
When the slider is at its minimum value, the width
of the progress
is 0
(it should be one track radius $track-r
). When the slider is at its maximum value, the width
of the progress
is the same as that of its parent $input-w
(it should be the width
of its parent minus twice the amount var(--dif)
by which the visual track is offset inwards from the slider's edges). What's worse, the left edge of this progress
is always attached to the left edge of the actual slider, outside the visial track. And as detailed here, the problem is even worse when the thumb
is smaller and all tricks that may be used so solve the problem come with problematic side effects themselves.
So here's what we have for the initial slider: a solution that's hacky in WebKit browsers where the thumb
is a child of the track
and doesn't show a progress without JS in any browser.
And this is for a very simple flat slider. It gets worse for realistic-looking ones.
Related spec issue: https://github.com/whatwg/html/issues/7570
Thank you for proposing input[type="range"] styling for inclusion in Interop 2024.
We wanted to let you know that this proposal was not selected to be part of Interop 2024. This is because we got many more proposals than we could include in this year's project. Note that individual vendors may nevertheless choose to advance work in this area during the forthcoming year. We would welcome this proposal being resubmitted again next year, if necessary.
For an overview of our process, see proposal selection. Thank you again for contributing to Interop 2024!
Posted on behalf of the Interop team.
Description
The ability to style
input type=range
controls, as per https://github.com/w3c/csswg-drafts/issues/4410#issuecomment-1720875895Although this is still just a WG resolution at this stage, we think the overall author demand for better form control styling warrants consideration of possible low-hanging fruit for Interop 2024. This proposal is a blocker to more significant form styling work such as two-state switch controls. As part of this proposal we will commit to helping progress the spec and test situation before the start of the Interop period.
Specification
https://drafts.csswg.org/css-pseudo-4/ (but not yet)
Open Issues
https://github.com/w3c/csswg-drafts/issues/4410
Tests
None yet
Current Implementations
Standards Positions
No response
Browser bug reports
No response
Developer discussions
No response
Polls & Surveys
https://2023.stateofcss.com/en-US/usage/#css_pain_points shows form element styling as by far the most significant concrete pain point for CSS authors, behind only the general "browser compatibility".
Existing Usage
No response
Workarounds
https://github.com/w3c/csswg-drafts/issues/4410#issuecomment-1087244943 and followups for some of the approaches authors currently use, and their limitations.
Accessibility Impact
No response
Privacy Impact
No response
Other
No response