Source Code

<div class="demo">
  <div class="slider-block">
    <label class="slider-label" for="volume">Volume</label>
    <input type="range" id="volume" class="range volume-range" min="0" max="100" value="65" />
  </div>
  <div class="slider-block">
    <label class="slider-label" for="brightness">Brightness</label>
    <input type="range" id="brightness" class="range brightness-range" min="0" max="100" value="40" />
  </div>
  <div class="slider-block">
    <label class="slider-label" for="quality">Quality</label>
    <input type="range" id="quality" class="range quality-range" min="0" max="100" value="80" step="20" />
  </div>
</div>

CSS Only Styled Range Slider — Vendor Pseudo-Elements, No JavaScript

Styled Range Slider — CSS Only Vendor Pseudo-Elements (No JavaScript) · Forms · Plain HTML & CSS · Live preview

What's included

Features

Real native <input type="range"> underneath — full keyboard, screen reader, and form-submission support preserved
::-webkit-slider-runnable-track / ::-webkit-slider-thumb fully restyle the WebKit rendering
::-moz-range-track / ::-moz-range-thumb / ::-moz-range-progress restyle Firefox, including a native live-filling progress pseudo-element
Three visually distinct variants (Volume, Brightness, Quality) sharing one underlying pseudo-element pattern
accent-color set as a sensible baseline fallback wherever vendor pseudo-elements aren't applied
Thumb hover scale feedback via a lightweight transform transition on both engines
Honest, explicit limitation noted and explained: the WebKit fill position is static, not live-updating, without JS
Zero JavaScript — the slider drags, focuses, and submits correctly in script-stripped environments

About this UI Snippet

Custom-Styled Native Range Slider — WebKit and Firefox Vendor Pseudo-Elements

Screenshot of the Styled Range Slider — CSS Only Vendor Pseudo-Elements (No JavaScript) snippet rendered live

The native <input type="range"> is genuinely useful — real keyboard support (Arrow keys, Page Up/Down, Home/End), real form submission, real accessibility semantics — but its default appearance is inconsistent and hard to theme across browsers. This snippet keeps the native element and its behavior intact, and restyles only its rendering, using the vendor-prefixed pseudo-elements each browser engine exposes for exactly this purpose.

Why appearance: none is the starting point

-webkit-appearance: none (and the standard appearance: none) strips the browser's default slider chrome entirely, leaving a bare, unstyled <input>. Without this reset, none of the pseudo-element rules below reliably override the native rendering, because the default UI is drawn by the browser's own theme engine rather than styleable elements in the normal cascade.

Two separate pseudo-element vocabularies

There is no single standard set of pseudo-elements for range input parts — each engine defines its own, and this snippet targets both by necessity. WebKit browsers (Chrome, Safari, Edge) expose ::-webkit-slider-runnable-track for the groove and ::-webkit-slider-thumb for the draggable handle. Firefox exposes ::-moz-range-track for the groove, ::-moz-range-thumb for the handle, and — usefully — ::-moz-range-progress, a pseudo-element WebKit has no equivalent for for that automatically fills from the track's start up to the current thumb position.

How the colored fill-track effect is achieved differently per engine

In Firefox, ::-moz-range-progress does the filled-portion rendering for free — the browser handles clipping it to the current value. WebKit has no such pseudo-element, so the filled effect there is faked with a linear-gradient hard-stop on ::-webkit-slider-runnable-track: a CSS custom property --fill marks the percentage where the gradient switches from the accent color to the track's neutral gray. Because this snippet uses no JavaScript, --fill is set as a static fallback value per slider (matching each value attribute's initial position) rather than updating live as the thumb moves — which is the honest limitation explained below.

The honest tradeoff: no live-updating fill or value label

This is a real, worth-stating limitation: raw CSS has no way to read a range input's current numeric value and feed it back into a content string or a custom property as the user drags the thumb. The attr() CSS function can only read *static* HTML attributes, not the live, JS-updated value property a range input holds while being dragged — and even the attribute (value="65" in the markup) never changes as the user interacts, since dragging updates the DOM property, not the HTML attribute. So the WebKit gradient fill's hard-stop percentage in this snippet reflects only the *initial* value baked into the CSS, not a continuously-accurate live position — the fill will look correct at rest but won't visually track the thumb while dragging in WebKit browsers. A live-updating fill or an on-screen numeric readout genuinely requires either JavaScript (listening for the input event and updating a CSS custom property or text node) or accepting Firefox's native ::-moz-range-progress behavior, which does update live natively in that one engine only.

Three variants, one mechanism

The Volume, Brightness, and Quality sliders reuse the identical pseudo-element structure with different colors, thumb shapes (circular vs. a rounded-square "chunky" thumb for Quality), and track heights — demonstrating that once the vendor pseudo-element pattern is in place, theming additional variants is a matter of overriding a handful of color and sizing values per class, not rebuilding the slider from scratch.

Where a native range input still wins over a JS-built slider

Even accepting the live-fill limitation, a native <input type="range"> keeps full keyboard operability, screen-reader announcement of the current value, and real form submission — all free from the browser — none of which a from-scratch JS/div-based slider gets without deliberately reimplementing it. And because it needs no JavaScript to render, drag, or submit its value, it continues to work in sanitized CMS content, email, and script-stripped embeds, where a div-based custom slider control could not function at all.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain exactly why CSS cannot animate the WebKit gradient fill to match the live dragged value, and to contrast that with why Firefox's ::-moz-range-progress can — this is a real, engine-level capability gap worth understanding rather than working around incorrectly. It's also a good prompt for adding a minimal, clearly-scoped JavaScript enhancement (a single input-event listener updating a CSS custom property) as an optional add-on layered on top of this otherwise JS-free base, and for building a dual-thumb range variant.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a set of three custom-styled native <input type="range"> sliders using only HTML and CSS — no JavaScript, no onchange/oninput attributes, no <script> tags — that remain fully functional native range inputs.

Requirements:
- Reset each slider's default browser appearance using appearance: none (and the -webkit- prefixed equivalent) before applying any custom styling.
- Restyle the track and thumb for WebKit browsers using ::-webkit-slider-runnable-track and ::-webkit-slider-thumb, and separately restyle them for Firefox using ::-moz-range-track and ::-moz-range-thumb, including Firefox's ::-moz-range-progress pseudo-element for its native live-filling track segment.
- Create a colored "filled" track effect on the WebKit side using a linear-gradient hard-stop on the track pseudo-element, and explicitly acknowledge in accompanying documentation/comments that this fill position is static (matching the initial value) rather than live-updating during a drag, since CSS alone cannot read a range input's live dragged value.
- Style at least two visually distinct variants (e.g. a default slider and a differently-colored, differently-shaped "volume" slider) sharing the same underlying pseudo-element technique.
- Ensure the sliders remain fully keyboard-operable (arrow keys, Home/End) and show a visible focus outline, since no custom JavaScript keyboard handling should be added or needed.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Reset appearance firstApply -webkit-appearance: none and appearance: none to the input before any pseudo-element rules, or the browser default chrome will still show through.
  2. 2
    Style both vendor prefixesYou must write both the ::-webkit- and ::-moz- rule sets — neither browser engine reads the other's vendor-prefixed pseudo-elements.
  3. 3
    Set the static --fill percentage to match your default valueThe WebKit gradient fill hard-stop should match the min/max/value you set on the input so the resting appearance looks correct.
  4. 4
    Use accent-color as a baseline fallbackaccent-color gives a reasonably themed slider in browsers or contexts where the vendor pseudo-elements are overridden or unavailable.
  5. 5
    Decide if you need a live value readoutIf users must see the live numeric value as they drag, you need JavaScript — this snippet intentionally does not fake that with CSS alone; see the FAQ for why.

Real-world uses

Common Use Cases

Media Player Controls
Volume and playback position sliders themed to match a player's brand, without a JS slider library dependency
Settings and Preferences Screens
Brightness, quality, or sensitivity sliders in a settings panel needing a consistent custom look across browsers
SHOP
Product Configurator Sliders
A styled quantity or customization-level slider on a product page rendered inside a sanitized CMS block
Interactive Documentation Widgets
A themed range control embedded in Markdown-rendered docs where scripts are stripped from the output
Learning Vendor Pseudo-Elements
A clear side-by-side reference for the WebKit vs. Firefox range-input styling vocabularies
Related: Dynamic Field Array — Add/Remove Repeatable Form Rows
See the Dynamic Field Array — Add/Remove Repeatable Form Rows for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

CSS cannot read a range input's live value and feed it back into a gradient hard-stop without JavaScript — attr() only reads the static HTML attribute, which does not update as the user drags. The gradient fill shown here reflects only the initial resting value; live-updating it requires listening to the input event in JavaScript.

No — Firefox's ::-moz-range-progress pseudo-element is maintained natively by the browser and does track the live value as the user drags, with no JS needed. This is a genuine engine capability difference, not a bug in this snippet.

That specifically requires JavaScript — listen for the input event on the range element and write event.target.value into a text node. There is no reliable pure-CSS way to display a range input's live numeric value.

Each rendering engine only recognizes its own vendor-prefixed pseudo-elements; ::-webkit-slider-thumb has no effect in Firefox and ::-moz-range-thumb has no effect in Chrome/Safari/Edge, so both rule sets are required for consistent cross-browser styling.

accent-color gives a reasonably themed, browser-consistent slider with a single CSS property and no vendor-prefix duplication, but it offers far less control over exact thumb shape, track height, and gradient fill effects than the full pseudo-element approach used here.

Yes — none of the styling touches the input's underlying behavior. Arrow keys, Page Up/Down, Home/End, and tab focus all continue to work exactly as they do on an unstyled native range input.