Skip to content
Open UI

Enhanced Range Input (Explainer)

Table of Contents

Background

The <input type="range"> element has long been a part of HTML forms, providing a slider control for selecting numeric values. However, its current implementation lacks flexibility in styling and functionality, leading developers to often resort to custom implementations. These custom solutions can result in reduced performance, reliability, and accessibility compared to native form controls.

The web development community has expressed a need for more customizable and feature-rich range inputs, particularly for use cases such as price range selectors, date/time range pickers, and other multi-value selection scenarios.

Goals

  1. Standardize the anatomy and structure of range inputs across browsers.
  2. Provide a comprehensive styling API for range inputs.
  3. Introduce support for multi-handle range inputs through a new <rangegroup> element.
  4. Enhance accessibility for range inputs, especially for multi-handle scenarios.
  5. Improve the overall user experience and developer ergonomics for range inputs.

Current State of Range Styling

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 */ }

Firefox:

::-moz-range-thumb { /* Styles the thumb */ }
::-moz-range-track { /* Styles the track */ }
::-moz-range-progress { /* Styles the progress/fill below the thumb */ }

IE/Edge:

::-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.

There currently is running a draft solution for this found at: (https://drafts.csswg.org/css-forms/)[https://drafts.csswg.org/css-forms/]. We would at least like to maintain the same naming convention:

  • ::slider-track: The main track along which the thumb(s) move.
  • ::slider-fill: The filled portion of the track, typically between the minimum value and the thumb (or between thumbs for multi-handle ranges).
  • ::slider-thumb: The draggable handle(s) used to select values.

Taking this further, we would like to propose new pseudo elements following the same convention:

  • ::slider-segment: Sections of the track between handles in multi-handle ranges.
  • ::slider-tick: Optional tick marks along the track for value representation. (when datalist is paired.)
  • ::slider-tick-label: Labels associated with tick marks. (when datalist is paired.)

This would follow the idea of the appearance: base value from the CSS Forms specification:

input[type="range"] {
    appearance: base;
}

This opt-in approach allows developers to access enhanced styling capabilities while maintaining a partial backwards compatibility.

Multi-Handle Support with <rangegroup>

Based on community feedback and discussions, we propose a new approach for multi-handle functionality. Instead of adding a new attribute to the existing <input type="range"> element or adding a new type, we believe a new <rangegroup> element could offer us the correct flexibility. This would be a wrapper element that can contain multiple <input type="range"> elements.

A live proof-of-concept demonstrating these ideas can be found at: https://brechtdr.github.io/enhanced-range-slider-poc/

This approach offers several advantages:

  • Easy feature detection
  • Better progressive enhancement as browsers that don’t support <rangegroup> will still display individual range inputs
  • Clearer separation of concerns between individual range handles
  • More intuitive form submission with distinct name/value pairs
  • Simplified styling and attribute management

Example usage:

<rangegroup>
  <legend>Temperature Range</legend>
  <label>Minimum Temperature
    <input type="range" name="temp-min" value="-25">
  </label>
  <label>Maximum Temperature
    <input type="range" name="temp-max" value="15">
  </label>
</rangegroup>

The <rangegroup> element would visually present these as a single slider with multiple handles on a common track, while maintaining the individual inputs for form submission and JavaScript interaction.

Accessibility Enhancements

To improve accessibility, especially for multi-handle ranges, the <rangegroup> element is designed to work with established HTML patterns for labeling and grouping form controls.

Individual thumbs must be focusable and operable via the keyboard. The <rangegroup> approach achieves this by using actual <input type="range"> elements for each handle, preserving their native accessibility features. Users can tab between handles, and each handle maintains its standard keyboard controls (e.g., arrow keys for adjustment).

Labeling

A robust labeling strategy is crucial for users of assistive technologies. We propose a pattern that mirrors the accessible and progressively enhanced structure of <fieldset> and <legend>.

Group Label: The <rangegroup> element should be labeled by including a <legend> element as a direct child. This provides the accessible name for the entire group of sliders.

Individual Handle Labels: Each <input type="range"> within the group should have its own accessible name. This can be provided by wrapping the <input> within a <label> element (implicit labeling), which is the recommended approach. Alternatively, standard methods like an aria-label or aria-labelledby attribute on the <input> can be used.

Example Implementation

<rangegroup>
  <legend>Price Range</legend>

  <label>Minimum Price
    <input type="range" name="price-min" value="25">
  </label>

  <label>Maximum Price
    <input type="range" name="price-max" value="75">
  </label>
</rangegroup>

Focus Behavior

This approach also defines clear focus behavior.

  • Clicking a <label> will focus its associated <input>’s handle, as is standard for form controls.
  • The <legend> element provides a group name but does not have a default click-to-focus behavior. This is appropriate, as there are multiple focusable targets within the group, avoiding ambiguity.

Progressive Enhancement

The <rangegroup> approach is designed with progressive enhancement as a core principle. In browsers that do not support the element, the markup gracefully degrades to standard, functional HTML form controls.

For example, consider a price range selector:

<rangegroup>
  <legend>Price Range</legend>

  <label>Minimum Price
    <input type="range" name="price-min" value="100" min="0" max="1000">
  </label>

  <label>Maximum Price
    <input type="range" name="price-max" value="750" min="0" max="1000">
  </label>
</rangegroup>

In a supporting browser: This renders as a single slider track with two draggable handles, labeled “Minimum Price” and “Maximum Price”, under a group heading of “Price Range”.

In a non-supporting browser: This renders as two separate, standard <input type="range"> sliders. Each slider is correctly labeled, fully functional, and accessible. The <legend> will appear as plain text, still providing context for the group of inputs that follow. This ensures the form remains usable and understandable for all users, regardless of browser support, without requiring JavaScript polyfills for basic functionality.

Design Consideration: range vs. number inputs

A consideration was raised whether <input type="number"> might serve as a better base for progressive enhancement than multiple <input type="range"> elements. While <input type="number"> provides a valid fallback for setting a range, we believe using base <input type="range"> elements better preserves the design intent. A range input is designed for selecting an approximate value within a scale, where the user is more interested in the position along the track than a precise number. This aligns with the visual metaphor of the enhanced <rangegroup> component. Using <input type="range"> as the fallback provides a more equitable, if not identical, user experience.

Datalist Integration

We propose standardizing and enhancing the integration of <datalist> with range inputs and rangegroups. This will allow for consistent implementation of tick marks and predefined values across browsers. For <rangegroup> elements, the <datalist> can be associated with the group as a whole, and all contained range inputs will share the same tick marks:

<rangegroup name="price-range" min="0" max="100" list="price-ticks">
    <legend>Price Range</legend>
    <label>Minimum Price
        <input type="range" name="price-min" min="0" max="50" value="20">
    </label>
    <label>Maximum Price
        <input type="range" name="price-max" min="50" max="100" value="80">
    </label>
</rangegroup>
<datalist id="price-ticks">
    <option value="0">$0</option>
    <option value="25">$25</option>
    <option value="50">$50</option>
    <option value="75">$75</option>
    <option value="100">$100</option>
</datalist>

Alternatively, datalist could be provided on each input as well. While these won’t render inside of the <rangegroup>, they would render on individual inputs when there is no support for the feature.

Multi-Color Range Segments

To support different colors between handles in a <rangegroup>, we introduce the ::slider-segment pseudo-element. This allows for granular control over the appearance of each segment in the range, enabling rich user interfaces such as budget allocators where each segment can represent a different category. Segments get numbered based on their start and end positions with 1 being at the start (based on writing mode). Example usage:

rangegroup::slider-segment(1) {
    background-color: #FF5733;
}

/* Style the second segment (between the first and second handles) */
rangegroup::slider-segment(2) {
    background-color: #33FF57;
}

/* Style the third segment (between the second handle and the end) */
rangegroup::slider-segment(3) {
    background-color: #3357FF;
}

The amount of segments get crated based on the amount of thumbs.

Detailed Design

HTML Attributes

<rangegroup> Attributes

  • min: Specifies the minimum value for the entire range group, defining the lower bound of the track.
  • max: Specifies the maximum value for the entire range group, defining the upper bound of the track.
  • name: The name of the range group, used for identification.
  • list: Links the range group to a <datalist> element, providing tick marks or predefined values.
  • stepbetween: Defines the minimum distance between handles in a range group.

<input type="range"> Attributes (within a <rangegroup>)

These attributes function as they normally would, but their values are constrained by the parent <rangegroup>.

  • min: The minimum value for this specific handle. Its effective range is determined by its interaction with the parent <rangegroup>.
  • max: The maximum value for this specific handle. Its effective range is determined by its interaction with the parent <rangegroup>.
  • value: The current value of this handle. This still updates individually by changing the thumb.
  • name: The name of this specific range input for form submission.
  • step: The step increment for this specific handle. This results in the steps of the thumb in a rangegroup environment, meaning that each thumb could potentially have different steps.

min and max Attribute Interaction

The <rangegroup> element and its child <input type="range"> elements can both have min and max attributes. Their interaction is key to providing both flexibility and a coherent progressive enhancement story.

  • <rangegroup> min and max: These attributes define the overall coordinate space of the slider. They determine the full visual range of the track.
  • <input> min and max: These attributes define the allowable value range for that specific handle.

The effective range for any given handle is the intersection of its own constraints and the parent <rangegroup>’s constraints. The tightest constraint always wins. In other words, a handle’s value must be greater than or equal to both its own min and the group’s min, and less than or equal to both its own max and the group’s max.

  1. If a handle’s range is within the group’s range (e.g., rangegroup max="100", input max="70"): The track will render from 0 to 100, but the handle will be prevented from moving past 70. This is a desirable pattern for use cases like a seek bar for a live video stream, where the total buffer is visible but the user cannot seek into the future.
  2. If a handle’s range exceeds the group’s range (e.g., rangegroup max="100", input max="200"): The handle’s movement will be clamped by the <rangegroup>’s bounds. The handle will not be able to exceed a value of 100. This ensures the component’s visual representation is the source of truth and prevents invalid states.

This model supports progressive enhancement. In a non-supporting browser, the individual inputs with their min and max attributes function correctly as standalone sliders. When <rangegroup> is supported, it unifies the UI and can apply overarching constraints without breaking the underlying inputs.

Track Click Behavior

To provide an intuitive single-pointer interaction model, the <rangegroup> component’s track-clicking behavior mirrors that of the native <input type="range">.

When a user clicks or taps anywhere on the slider’s track, the component identifies the thumb closest to the click location and moves it to that position.

This approach offers several benefits:

  • Familiarity: It aligns with user expectations based on the standard behavior of single-handle sliders and some popular component libraries.
  • Direct Manipulation: It provides a quick and direct way for users to move any handle to a desired point with a single click, without needing to drag.

While this interaction is powerful, it’s worth noting that on touch devices with coarse pointers, care must be taken to avoid accidental adjustments. However, we believe the benefit of adhering to a well-established browser-native pattern provides the most valuable and predictable user experience.

CSS Properties and Pseudo-elements

To address the current fragmentation and provide a unified styling API, we propose the following pseudo-elements, aligning with the CSS Forms specification:

  • ::slider-track: Represents the main track of the range group.
  • ::slider-segment: Represents sections of the track between handles.
  • ::slider-thumb: Represents the draggable handles within the group.
  • ::slider-tick: Represents individual tick marks on the range group with attached datalist.
  • ::slider-tick-label: Represents the label associated with each tick mark of a datalist option.

Example usage:

/* Styling a basic range input */
input[type="range"] {
    appearance: base;
}

input[type="range"]::slider-track {
    height: 4px;
    background-color: #ddd;
}

input[type="range"]::slider-fill {
    background-color: #4CAF50;
}

input[type="range"]::slider-thumb {
    width: 20px;
    height: 20px;
    background-color: #2196F3;
    border-radius: 50%;
}

rangegroup {
    appearance: base;
}

rangegroup::slider-track {
    height: 6px;
    background-color: #f0f0f0;
}

rangegroup::slider-segment(1) {
    background-color: #FF5733;
}

rangegroup::slider-segment(2) {
    background-color: #33FF57;
}

rangegroup input[type="range"]::slider-thumb {
    width: 24px;
    height: 24px;
    background-color: #2196F3;
    border-radius: 50%;
}

JavaScript API

The existing JavaScript API for <input type="range"> remains unchanged inside of the gorup.

For groups specifically we’d like to introduce a few new ones:

  • values: A property that returns an array of values for all contained range inputs.
  • inputs: A property that returns a collection of the contained range input elements. A shorthand for querySelectorAll
  • getRangeInput(index): Returns the range input at the specified index.
  • setRangeValue(index, value): Sets the value for a specific handle.

Example usage:

const rangeGroup = document.querySelector('rangegroup[name="price-range"]');

// Getting values
console.log(rangeGroup.values); // [100, 750]

// Getting inputs
const inputs = rangeGroup.inputs;
console.log(inputs.length); // 2

// Setting a specific handle's value
rangeGroup.setRangeValue(0, 150);
console.log(rangeGroup.values); // [150, 750]

Extra Examples

Dual-Handle Range Group

 <rangegroup name="price-range" min="0" max="1000">
    <legend>Price Range</legend>
    <label>Minimum Price
        <input type="range" name="price-min" min="0" max="500" value="250" step="10">
    </label>
    <label>Maximum Price
        <input type="range" name="price-max" min="500" max="1000" value="750" step="10">
    </label>
</rangegroup>
rangegroup {
    appearance: base;
    width: 300px;
}

rangegroup::slider-track {
    height: 6px;
    background-color: #f0f0f0;
}

rangegroup::slider-segment(1) {
    background-color: #ddd;
}

rangegroup::slider-segment(2) {
    background-color: #4CAF50;
}

rangegroup::slider-segment(3) {
    background-color: #ddd;
}
 rangegroup input[type="range"]::slider-thumb {
    width: 24px;
    height: 24px;
    background-color: #2196F3;
    border-radius: 50%;
}

Multi-Handle Range Group with Colored Segments

<rangegroup name="temperature-range" min="-100" max="100">
    <legend>Temperature Range</legend>
    <label>Low
        <input type="range" name="temp-low" min="-100" max="-25" value="-50">
    </label>
    <label>Medium
        <input type="range" name="temp-medium" min="-25" max="25" value="0">
    </label>
    <label>High
        <input type="range" name="temp-high" min="25" max="100" value="75">
    </label>
</rangegroup>
rangegroup {
    appearance: base;
    width: 300px;
    height: 20px;
}

rangegroup::slider-track {
    height: 10px;
    background-color: #ddd;
}

rangegroup input[type="range"]::slider-thumb {
    width: 20px;
    height: 20px;
    background-color: #2196F3;
    border-radius: 50%;
}

rangegroup::slider-segment(1) {
    background-color: #FF5733;
}

rangegroup::slider-segment(2) {
    background-color: #33FF57;
}

rangegroup::slider-segment(3) {
    background-color: #3357FF;
}

rangegroup::slider-segment(4) {
    background-color: #FF33A8;
}

Considerations and Open Questions

  • Should there be a limit on the number of range inputs that can be included in a <rangegroup>? Based on discussions, we’ve decided not to impose a limit, leaving it to authors to determine the appropriate number for their use case. A separate issue will be created to address potential accessibility concerns with a large number of handles.
  • How should we handle thumb collision in multi-handle ranges?
  • What is the best way to handle keyboard navigation for multi-handle ranges? While tabbing between inputs is natural, should there be additional keyboard shortcuts for more efficient navigation?
  • Should we consider additional pseudo-elements for more granular styling control?
  • How should we handle vertical orientation for range inputs? The CSS Forms specification introduces a slider-orientation property that could be adopted for this purpose.
  • Should we provide options for automatic tick mark generation (e.g., evenly spaced) without requiring a <datalist>? These could be auto-generated based on a step attribute of a rangegroup
  • How can we ensure that tick marks and labels remain legible and usable on small-screen devices or with a large number of ticks? Is this author responsibility?
  • Should we provide a way to programmatically set segment colors with a JavaScript API?
  • What should the behavior be when a handle’s value is updated programmatically to a value that would violate the constraints imposed by stepbetween or other range limits?
  • How should form submission work for <rangegroup> elements? Should they submit as a single value (comma-separated) or as multiple values with the same name?