Examples from Open UI research
Currently, the state of styling <input type="range">
across browsers is inconsistent and challenging. Different browsers use their own pseudo-elements for styling range inputs, leading to a fragmented landscape for developers. Here’s an overview of the current state:
::-webkit-slider-thumb { /* Styles the thumb */ }
::-webkit-slider-runnable-track { /* Styles the track */ }
::-moz-range-thumb { /* Styles the thumb */ }
::-moz-range-track { /* Styles the track */ }
::-moz-range-progress { /* Styles the progress/fill below the thumb */ }
::-ms-thumb { /* Styles the thumb */ }
::-ms-track { /* Styles the track */ }
::-ms-fill-lower { /* Styles the progress/fill below the thumb */ }
::-ms-fill-upper { /* Styles the fill above the thumb */ }
This fragmentation makes it difficult for developers to create consistent range input styles across browsers, often requiring browser-specific code or fallbacks.
We propose standardizing the internal structure of range inputs across all browsers. This will ensure consistent styling and behavior, regardless of the browser implementation.
The standardized anatomy includes:
::range-track
: The main track along which the thumb(s) move.::range-fill
: The filled portion of the track, typically between the minimum value and the thumb (or between thumbs for multi-handle ranges).::range-thumb
: The draggable handle(s) used to select values.::range-segment
: Sections of the track between handles in multi-handle ranges.::range-tick
: Optional tick marks along the track for value representation. (when datalist is paired.)::range-tick-label
: Labels associated with tick marks. (when datalist is paired.)To enable comprehensive styling of range inputs, we propose introducing a new CSS appearance
value and a set of pseudo-elements:
input[type="range"] {
appearance: base-range;
}
This opt-in approach allows developers to access enhanced styling capabilities while maintaining backwards compatibility.
We propose extending the range input to support multiple handles, starting with dual-handle support as the most common use case. This will be achieved through new HTML attributes and corresponding JavaScript APIs.
To improve accessibility, especially for multi-handle ranges, we propose making individual thumbs focusable and providing clear audio cues for screen readers.
The reason for opting in is to ensure backward compatibility with existing range inputs. Since current implementations are a mess, it feels like a safer bet. Browsers that do not support the new features will fall back to the current behavior of range inputs, and it opens up the window of targeting support via CSS (@supports).
For multi-handle ranges, browsers that do not support the feature will treat the input as a standard single-handle range, using the value provided in the value
attribute. To ensure proper fallback behavior, we recommend using both value
and values
attributes:
<input type="range" handles="3" value="20" values="20,40,60" min="0" max="100" minhandles="0,30,40" maxhandles="20,60,80" />
More details about the attributes can be found in the detailed design.
This idea could potentially help with polyfills as these can change the new values attribute to data attributes for backward compatability.
We propose standardizing and enhancing the integration of <datalist>
with range inputs. This will allow for consistent implementation of tick marks and predefined values across browsers, especially when used with the appearance: base-range
styling.
Key features:
<datalist>
options as tick marks on the range track.::range-tick
pseudo-element.::range-tick-label
pseudo-element.<datalist>
with multi-handle range inputs. All thumbs should be able to snap to the options provided.To support different colors between handles in multi-range inputs, we introduce the ::range-segment
pseudo-element. This allows for granular control over the appearance of each segment in the range.
Example usage:
input[type="range"][handles="3"] {
appearance: base-range;
}
/* Style the first segment (between the start and the first handle) */
input[type="range"][handles="3"]::range-segment:nth-child(1) {
background-color: #FF5733;
}
/* Style the second segment (between the first and second handles) */
input[type="range"][handles="3"]::range-segment:nth-child(2) {
background-color: #33FF57;
}
/* Style the third segment (between the second handle and the end) */
input[type="range"][handles="3"]::range-segment:nth-child(3) {
background-color: #3357FF;
}
handles
: Specifies the number of handles for the range input. Default is 1.<input type="range" handles="2" min="0" max="100" value="25" values="25,75" />
value
: For single-handle ranges and as a fallback for multi-handle ranges, contains a single value.<input type="range" handles="2" min="0" max="100" value="25" values="25,75" />
values
: An attribute specifically for multi-handle ranges, containing a comma-separated list of values.<input type="range" handles="2" min="0" max="100" value="25" values="25,75" />
stepbetween
: Defines the minimum distance between handles in a multi-handle range.<input type="range" handles="2" min="0" max="100" value="25" values="25,75" stepbetween="10" />
minhandles
and maxhandles
: Define the minimum and maximum values for each handle, as comma-separated lists.<input type="range" handles="3" value="20" values="20,40,60" min="0" max="100" minhandles="0,30,40" maxhandles="20,60,80" />
list
: Links the range input to a <datalist>
element, providing tick marks or predefined values.<input type="range" min="0" max="100" list="tickmarks">
<datalist id="tickmarks">
<option value="0" label="0%"></option>
<option value="25" label="25%"></option>
<option value="50" label="50%"></option>
<option value="75" label="75%"></option>
<option value="100" label="100%"></option>
</datalist>
To address the current fragmentation and provide a unified styling API, we propose the following pseudo-elements:
::range-track
: Represents the main track of the range input.::range-fill
: Represents the filled portion of the track.::range-thumb
: Represents the draggable handle(s).::range-tick
: Represents individual tick marks on the range input.::range-tick-label
: Represents the label associated with each tick mark. This pseudo-element should be able to handle the content
property for custom content. By default, the content is the value of the tick.::range-segment
: Represents sections of the track between handles in multi-handle ranges.These pseudo-elements can be used in combination with other CSS selectors, such as :nth-child()
, to provide granular control over individual ticks, labels, and segments.
Example usage:
input[type="range"] {
appearance: base-range;
}
input[type="range"]::range-track {
height: 4px;
background-color: #ddd;
}
input[type="range"]::range-fill {
background-color: #4CAF50;
}
input[type="range"]::range-thumb {
width: 20px;
height: 20px;
background-color: #2196F3;
border-radius: 50%;
}
/* Style all ticks */
input[type="range"]::range-tick {
width: 2px;
height: 8px;
background-color: #999;
}
/* Style every fifth tick differently */
input[type="range"]::range-tick:nth-child(5n) {
height: 12px;
background-color: #333;
}
/* Style all tick labels */
input[type="range"]::range-tick-label {
font-size: 12px;
color: #666;
}
/* Style labels for every fifth tick differently */
input[type="range"]::range-tick-label:nth-child(5n) {
font-weight: bold;
color: #333;
}
/* Style segments in a multi-handle range */
input[type="range"][handles="3"]::range-segment:nth-child(1) {
background-color: #FF5733;
}
input[type="range"][handles="3"]::range-segment:nth-child(2) {
background-color: #33FF57;
}
input[type="range"][handles="3"]::range-segment:nth-child(3) {
background-color: #3357FF;
}
values
: A new property that returns an array of numbers for multi-handle ranges.setRangeValue(index, value)
: Sets the value for a specific handle, without the need for a string.handlePositions
: Returns an array of handle positions as percentages.Example usage:
<input type="range" handles="2" min="0" max="100" value="25" values="25,75" />
const rangeInput = document.querySelector('input[type="range"][handles="2"]');
// Getting the value
console.log(rangeInput.value); // "25"
console.log(rangeInput.values); // [25,75]
// Setting the value
rangeInput.value = "40";
console.log(rangeInput.value); // "40"
console.log(rangeInput.values); // [30, 75]
// Setting a specific handle's value
rangeInput.setRangeValue(0, 35);
rangeInput.setRangeValue(1, 60);
console.log(rangeInput.value); // "40"
console.log(rangeInput.values); // [35, 60]
input[type="range"] {
appearance: base-range;
width: 200px;
}
input[type="range"]::range-track {
height: 4px;
background-color: #ddd;
}
input[type="range"]::range-fill {
background-color: #4CAF50;
}
input[type="range"]::range-thumb {
width: 20px;
height: 20px;
background-color: #2196F3;
border-radius: 50%;
}
<input type="range" min="0" max="100" value="50">
input[type="range"][handles="2"] {
appearance: base-range;
width: 300px;
}
input[type="range"][handles="2"]::range-track {
height: 6px;
background-color: #f0f0f0;
}
input[type="range"][handles="2"]::range-fill {
background-color: #4CAF50;
}
input[type="range"][handles="2"]::range-thumb {
width: 24px;
height: 24px;
background-color: #2196F3;
border-radius: 50%;
}
<input type="range" handles="2" min="0" max="1000" value="250" values="250,750" step="10">
input[type="range"] {
appearance: base-range;
width: 300px;
}
input[type="range"]::range-track {
height: 4px;
background-color: #ddd;
}
input[type="range"]::range-fill {
background-color: #4CAF50;
}
input[type="range"]::range-thumb {
width: 20px;
height: 20px;
background-color: #2196F3;
border-radius: 50%;
}
/* Style all ticks */
input[type="range"]::range-tick {
width: 2px;
height: 8px;
background-color: #999;
}
/* Style every fifth tick differently */
input[type="range"]::range-tick:nth-child(5n) {
height: 12px;
background-color: #333;
}
/* Style all tick labels */
input[type="range"]::range-tick-label {
font-size: 12px;
color: #666;
}
/* Style labels for every fifth tick differently */
input[type="range"]::range-tick-label:nth-child(5n) {
font-weight: bold;
color: #333;
}
<input type="range" min="0" max="100" step="5" list="percentages">
<datalist id="percentages">
<option value="0" label="0%"></option>
<option value="25" label="25%"></option>
<option value="50" label="50%"></option>
<option value="75" label="75%"></option>
<option value="100" label="100%"></option>
</datalist>
input[type="range"][handles="3"] {
appearance: base-range;
width: 300px;
height: 20px;
}
input[type="range"][handles="3"]::range-track {
height: 10px;
background-color: #ddd;
}
input[type="range"][handles="3"]::range-thumb {
width: 20px;
height: 20px;
background-color: #2196F3;
border-radius: 50%;
}
input[type="range"][handles="3"]::range-segment:nth-child(1) {
background-color: #FF5733;
}
input[type="range"][handles="3"]::range-segment:nth-child(2) {
background-color: #33FF57;
}
input[type="range"][handles="3"]::range-segment:nth-child(3) {
background-color: #3357FF;
}
<input type="range" handles="3" min="0" max="100" value="25" values="25,50,75">
::range-thumb
vs ::slider-thumb
)? Or use basic keywords based on recent unofficial draft by webkit<datalist>
with multi-handle range inputs?<datalist>
?minhandles
and maxhandles
attributes interact with the global min
and max
attributes?stepbetween
, minhandles
, or maxhandles
?