How does the native range input work?
Dragging, keyboard input and value tracking all belong to the browser. The user slides a thumb along a track, and CSS only restyles what is already there.
Behavior comes from a handful of ordinary HTML attributes. Min sets the lowest allowed value and defaults to 0, while max sets the highest and defaults to 100. Step controls the interval between values (1 by default, or the keyword any for no snapping). Value is the starting position, and it lands halfway between min and max unless you set it.
Those defaults come from the MDN Web Docs reference for the range input, revised in September 2026.
Set max equal to min, or below it, and the slider stops responding to the user. When a scripted value ignores the step, browsers round it to the nearest valid step and prefer the higher one on a tie.
Support is not a concern. MDN marks the range input as Baseline widely available, working across browsers since March 2017.
The control needs no JavaScript to work. Script only enters when the page has to display the current value, and the input event and the output element handle that.
The element also carries an implicit ARIA role of slider, so screen readers announce it without extra markup.
The pieces open to styling are the thumb, which is the draggable knob, and the track it slides in. There’s also the fill, the stretch of track behind the thumb, though browsers treat that one very unevenly. All of them are reached through vendor-specific pseudo-elements.
How to style the thumb and track
Strip the native look with appearance: none, then style the track and thumb through engine-specific pseudo-elements. Chrome, Safari and Edge use ::-webkit-slider-runnable-track and ::-webkit-slider-thumb. Firefox uses ::-moz-range-track and ::-moz-range-thumb.
- Set appearance: none on the input.
- Give the track an explicit height, a background and a border radius.
- Size the thumb explicitly, then offset it so it centers on the track in Chrome and Safari.
- Add :focus-visible and :active states to the thumb.
Reset the default appearance
Nothing written for the thumb or track applies until the native look is gone.
Set appearance: none on the input and on the thumb. Ana Tudor found in 2017 that setting it on only one of the two changes nothing in Chrome (CSS-Tricks).
The reset has a side effect. In her Chrome tests the thumb dropped from 10 by 21 pixels to 129 by 0, and the track and input collapsed with it, so the slider vanished until the thumb got explicit dimensions.
Style the track
Set the height explicitly. In Ana Tudor’s tests, Firefox defaulted to 0.2em, while Chrome derived the height from the thumb, so leaving it alone gives you two different sliders. The background is the unfilled color and can be a solid or a gradient. A border radius at half the height gives a pill-shaped track.
Size the width on the input, not the track. Chrome ignored a width set on the track in the same 2017 tests and stretched it to match the input.
Style the thumb
After the reset, Chrome aligns the top edge of the thumb with the top of the track, not its middle. Firefox centers it automatically.
The fix is a negative margin-top on the ::-webkit-slider-thumb rule only: track height minus thumb height, divided by 2. A 4px track with a 16px thumb takes margin-top: -6px.
If the track has a border, subtract twice its width from the track height before halving.
Round the thumb with border-radius: 50%. A soft shadow adds depth, and a box shadow generator speeds up tuning the values.
Leave margin-top out of the Firefox rule. Firefox already centers the thumb, so the same offset pushes it off the track.
Why does a range slider look different across browsers?
Each engine assembles the slider from different internal parts and gives them different names. Blink and WebKit nest the thumb inside the track, Gecko places track, fill and thumb side by side, and among current engines only Gecko offers a native fill.
| Part | Chrome, Safari, Edge | Firefox | IE and old Edge |
|---|
| Thumb | ::-webkit-slider-thumb | ::-moz-range-thumb | ::-ms-thumb |
| Track | ::-webkit-slider-runnable-track | ::-moz-range-track | ::-ms-track |
| Fill | None | ::-moz-range-progress | ::-ms-fill-lower |
The nesting changes behavior. In Ana Tudor’s 2017 CSS-Tricks tests, opacity, filter or transform on the track also hit the thumb in Chrome, but not in Firefox, where the two are siblings.
A selector list containing one pseudo-element the browser does not recognize is dropped whole. Write one rule per engine and never group the vendor selectors, a point the postcss-input-style documentation makes by citing the Selectors Level 3 specification.
Grouped selectors are a common source of cross-browser compatibility bugs in slider CSS. If a custom thumb shows in Chrome but not in Firefox, first confirm that a ::-moz-range-thumb rule exists and that it isn’t sharing a list with the WebKit selector.
Legacy ::-ms- rules only matter for Internet Explorer and the old EdgeHTML-based Edge. Microsoft retired the IE11 desktop app on June 15, 2022, per its retirement notice, so new projects can drop them.
Default dimensions differ too, so set an explicit width on the input rather than trusting the browser.
Test in Chrome, Safari and Firefox, and drag the thumb to both ends in each. Thumb travel is not identical. Chrome limits it to the track’s content box and Firefox to the input’s, according to the same CSS-Tricks analysis.
How far does accent-color go on a range slider?
The accent-color property recolors the slider with one declaration, and it stops at color. Track height, thumb size, thumb shape and tick marks stay with the browser.
The property is inherited, so accent-color on :root tints every supported control on the page. web.dev (Chrome team, 2021) lists checkbox, radio, range and progress among them.
The browser picks a contrast color to pair with your accent, and it does not change your accent in a dark theme. Set color-scheme: light dark so both themes get tinted, then check the accent’s color contrast against the track in each.
Contrast algorithms differ by engine. web.dev’s 2021 comparison showed Chrome 94 and Firefox 92 Nightly pairing the same accent with different contrast colors.
The appeal is obvious. It takes one declaration and no pseudo-elements, native keyboard behavior and focus handling stay intact, and the browser deals with contrast against the track.
The catch is that it only does color. Height, size and shape still need pseudo-elements. The browser, not you, chooses the companion color, you get no separate control over the unfilled track, and MDN flags the property as limited availability, not Baseline.
Sources disagree on support. Some tutorials call it well supported across browsers, while MDN’s page (revised September 2026) labels it limited availability.
MDN also notes that some browsers return a fixed value for accent-color: auto to reduce fingerprinting risk. Check Can I use before relying on it. An unsupported browser shows the default colors, so the page still works.
If the design only needs a brand color, stop here. If it needs a thicker track, a custom thumb or a different unfilled color, move to pseudo-element styling.
How to fill the track and show the current value
Store the thumb position in a CSS custom property, then use it as the hard stop of a linear-gradient on the track background. A few lines of script update the property on every input event, and an output element prints the number.
Fill the track with a custom property
The gradient needs one number, the thumb position as a fraction between 0 and 1, calculated as (value minus min) divided by (max minus min). Write it to a property such as –val from the input event listener, the approach CSS-Tricks documents for range inputs.
On the track background, put two gradient stops at the same position, with the accent color on the left and the unfilled color on the right. The stop position is a calc() that multiplies –val by the usable width. Set –val in the stylesheet to match the input’s starting value as well, or the fill starts empty until the script runs.
That stop position is where hand-built fills go wrong. The thumb stays inside the track, so at the minimum its center sits half a thumb width from the left edge, and a stop at 0% leaves that strip unfilled.
Take a 16px thumb on a 200px track. The center travels from 8px to 192px, so the stop is calc(8px + var(–val) * (100% – 16px)).
Firefox can use the same gradient. CSS-Tricks suggests it when the native fill pseudo-element grows in a way that looks wrong next to a round thumb.
A standard fill is on the way. The CSS Form Control Styling Module Level 1 editor’s draft (8 October 2026) defines ::slider-thumb, ::slider-track and ::slider-fill for input type range, with the fill nested inside the track.
The draft describes itself as provided for discussion only and subject to change. Keep the gradient until browser support is confirmed.
Show the value with output
The output element has been Baseline widely available since October 2018, according to MDN, so it needs no fallback.
Set its for attribute to the slider’s id, which marks the slider as the source of the value. Many browsers expose output as an aria-live region, so screen readers announce updates without moving focus. The output’s value is not sent with the form, though, so the range input has to carry the data.
Update it from the input event, not change. Input fires on every movement while the user drags, whereas change fires once on release.
A tooltip bubble reuses the same fraction. Position it with the same calc() so it follows the thumb center, and borrow the arrow and fade from standard CSS tooltip patterns.
The same draft adds control-value(), which would print the slider’s value from CSS alone, for example as pseudo-element content. The draft marks it as not ready for implementation.
Can a range slider have two handles?
Not natively. A range input has one thumb, so a two-handle slider needs either two overlapping inputs or a library.
The overlap technique starts with two input type=”range” elements stacked in one container, one for the minimum and one for the maximum. Give both the same fixed min and max, and never retarget one input’s bounds to the other’s value. Then set pointer-events: none on each input and pointer-events: auto on the thumb pseudo-elements.
That last step is not optional. Developer Mike Jolley found in 2019 that with events left on, only one of the two inputs responds to clicks.
Avoid restoring pointer events on the whole input when it gains focus, since the focused input would then cover the other thumb.
Where it breaks is mostly predictable. Nothing stops the lower thumb from passing the upper one, and the two can also land on the same value, so clamp both in script.
With pointer events off the track, a click between the thumbs does nothing either. WCAG 2.2 SC 2.5.7 requires a way to set the value without dragging, and W3C accepts a text input beside the slider as that alternative, so add two number fields.
The fill gets harder too. The gradient takes a from and a to fraction, each computed with the same thumb-travel calculation.
noUiSlider is a vanilla JavaScript option. Its documentation lists ARIA support, keyboard support, multi-touch and no dependencies, and a two-handle slider takes a start array such as [20, 80] with connect: true.
React projects have Radix UI Slider and Base UI Slider, both built for multiple thumbs. Base UI exposes collision behaviors of push, swap or none, so the crossing bug becomes a setting.
Overlapping inputs keep native keyboard and screen reader behavior on each thumb, need no dependency and leave styling entirely in your hands. The cost is that you write the clamping, the fill and the click alternative yourself. Users also hit two tab stops and hear two announced values for what is logically one range, and the per-engine pseudo-element rules get written twice.
A library flips that. Collision handling, tooltips and tick scales come built in, and keyboard and ARIA support are handled for you. In return you ship and update another script and stylesheet, you override its CSS for a custom look, and accessibility depends on the library’s implementation, so test it with a screen reader.
For a price filter on a product page, a library saves the clamping and click work. For a settings panel with one range pair, overlapping inputs are enough.
How to keep a custom range slider accessible
Keep the native input and put back any focus ring you remove. Past that, W3C sets thresholds for hit area, contrast and non-drag operation, and all of them apply to sliders directly.
- The target should be at least 24 by 24 CSS pixels (WCAG 2.2 SC 2.5.8, Level AA).
- Components and their states need 3:1 contrast against adjacent colors (non-text contrast, SC 1.4.11, Level AA).
- A single-pointer way to operate the control without dragging has to exist (SC 2.5.7, Level AA).
- On the keyboard, arrows change one step, Home and End jump to the first and last values, and Page Up and Page Down take larger steps (ARIA Authoring Practices Guide).
The WCAG figures come from W3C’s Understanding documents. The key list comes from the APG slider pattern.
On hit area, W3C’s Understanding document does not single out sliders, so the safe reading is a thumb of at least 24 by 24 pixels. The user agent exception only applies while the author leaves the browser’s default size alone, so a thin custom input has to pass on spacing instead.
Contrast works on the same logic. The thumb position tells the user the value, so the thumb needs 3:1 against the track, and the track should reach 3:1 against the page so the control can be found. W3C’s non-text contrast page names the slider thumb as state information.
Style :focus-visible on the thumb with an indicator that meets the same ratio. Removing the outline without a replacement is a documented failure, listed as technique F78 against SC 1.4.11, 2.4.7 and 2.4.13.
A visible label tied to the input with for and id is the baseline for accessible forms. When the number alone means little, such as a day of the week, aria-valuetext carries the readable string, for example “Monday”.
If you rebuild the slider from a div with role=”slider”, it needs the right ARIA properties (aria-valuenow, aria-valuemin, aria-valuemax), a label, and key handlers for every key in the list above. The native input provides all of it, which is the main reason not to rebuild.
The APG also warns that some touch-based assistive technologies may not operate widgets built to its slider pattern, since they rely on synthesized key events. Test any rebuilt slider on a touch device with a screen reader before shipping.
Which approach fits a range slider?
Start with accent-color, move to pseudo-element styling when the design needs a different size or shape, and bring in a library only for multiple thumbs, tick scales or non-linear ranges.
| Approach | Controls | Main limit | Best fit |
|---|
| accent-color | Thumb and fill color | Color only | Brand color on a standard slider |
| Pseudo-element styling | Size, shape, track, fill | One rule set per engine, gradient fill | Custom look, single thumb |
| Framework class | Prebuilt track and thumb | The framework’s own variables | Projects already on Bootstrap |
| Library | Handles, tooltips, tick scales | Script and stylesheet to maintain | Dual handles, non-linear scales |
The form-range class in Bootstrap’s form components styles the thumb and track to look the same across browsers. Bootstrap 5.3 defaults to a .5rem track and a 1rem thumb, both adjustable through Sass variables.
Tailwind projects hit the same boundary. Preline UI’s range slider docs style the native input and send tooltips, pips and multi-handle ranges to a separate plugin, and the setup relies on the Tailwind forms plugin by default.
Native styling has no way to add a second thumb. Non-linear scales are out too, because a native step moves in equal increments (MDN), while noUiSlider accepts non-linear ranges, which suits values spread from 10 to 10,000.
A library for a single-thumb slider is overkill. It adds a script and a stylesheet to a control the browser already provides.
The same goes for adding Bootstrap for one slider. Use the framework class only when the framework is already on the page.
CSS Range Slider FAQ
Is a range slider better than a number input?
Only when the exact value does not matter. A slider suits volume or brightness, where position carries the meaning, while a number input suits quantities a user has to type precisely.
The two also pair well. W3C accepts a text input beside a slider as the non-drag alternative under WCAG 2.2.
Do the ::-ms- pseudo-elements still matter?
Not for new projects. Rules such as ::-ms-thumb and ::-ms-fill-lower only worked in Internet Explorer and legacy Edge, and Chromium-based Edge reads the ::-webkit- rules instead.
Add them only if your supported browser list still includes those engines.
How do you make a vertical range slider?
Set writing-mode: vertical-lr on the input, then add direction: rtl so the minimum sits at the bottom. MDN notes that vertical form controls only gained full browser support in 2024. For the range input, its compatibility data lists Chrome 124, Firefox 120 and Safari 16.5, so check that table for older targets.
The slider-orientation property in the CSS Form Control Styling draft proposes a one-line alternative, but it remains a draft.
How do you add tick marks to a range slider with a datalist?
Point the input’s list attribute at a datalist, then add option elements whose values fall between min and max. Each option can carry a label attribute for the text under the tick.
Browsers differ in how they draw the ticks and labels. If the look must match everywhere, paint the ticks yourself with a repeating-linear-gradient on the track.
What to Test Before a CSS Range Slider Goes Live
A CSS range slider needs testing in Chrome, Firefox and Safari, plus a touch device with a screen reader. Chrome and Safari read one set of thumb rules, and Firefox reads another.
Check Firefox next to Chrome first, since that’s where the rules diverge. Then go keyboard only with the arrows, Home and End, and finish on a physical phone with a screen reader running.
The order follows cost. Engine differences show on screen in seconds, keyboard checks take a minute, and touch assistive technology needs a real device.
As of October 2026, the vendor-prefixed rules stay until ::slider-thumb, ::slider-track and ::slider-fill ship across engines. The CSS Working Group places unprefixed implementations at the Candidate Recommendation stage, and the module is still a Working Draft.
Until then, a native, accessible slider costs two rule sets to maintain.