Neu Slider
Real input type=range sliders dressed in neumorphism: a 10 px carved track, a violet gradient fill that stops right under the handle, a raised 24 px handle, value shown above. Mouse, finger and keyboard, 13 lines of JavaScript.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Glassmorphism has 47 effects, including 6 free. Explore the category →
3 usage examples



How it works
Each slider is an <input type="range" class="neu-slider"> inside a <label class="neu-slider-group"> that holds the title and the value. The native look is removed (appearance: none) and redrawn on each engine's pseudo-elements: the track (::-webkit-slider-runnable-track, ::-moz-range-track) is 10 px tall, border-radius: 5px, carved by #b8bec7 2px 2px 4px inset and #fff -2px -2px 4px inset; the handle (::-webkit-slider-thumb, ::-moz-range-thumb) is a 24 px raised disc (4 px / 8 px shadows, 6 px / 12 px on hover), with a grab then grabbing mouse cursor. The input itself is 24 px tall with a transparent background: the full height of the handle is a grab area.
The fill is a second background on the track: linear-gradient(135deg, #8b5cf6, #a78bfa) sized calc(12px + (100% - 24px) * var(--neu-p)). The handle's center travels from 12 px to the width minus 12 px; with this formula the gradient stops exactly under that center, at 0% as at 100%. The JavaScript is 13 lines: on every input event, it writes the ratio into --neu-p and the value into .neu-slider-value (“75%”). It applies to every .neu-slider on the page, with no id.
Why real <input type="range"> elements rather than drawn <div>s: a slider made of blocks set to 75% and 40% by an inline style promises a grip (cursor: grab) but does not move — dragging its handle 120 px leaves “75%”, and Tab finds nothing to reach. The native input brings dragging, touch, keyboard and screen-reader announcements for free; the CSS only dresses it. Measured in Chromium, Firefox and WebKit: dragging left from 75% gives 28%, a tap on the track at 20% gives 20%, arrow keys step by one, End and Home go to 100% and 0%. As for the whole family, .neu-slider-scene carries its surface (#e0e5ec, padding: 28px, border-radius: 24px).
Accessibility
- Keyboard and screen readers: these are native
input type="range"elements. Tab reaches them, arrows change the value by one, Page Up/Down by ten, Home and End go to the bounds. The accessible name comes from the wrapping label (“Volume”, “Luminosité”); the displayed value carriesaria-hidden="true"so it is not read twice, since the slider already announces its value. For a spoken unit (“75 percent”), addaria-valuetextin theupdatefunction. - Focus: with the keyboard, the handle gets a solid
#8b5cf6 0 0 0 3pxring (3.3:1 on the material, above the required 3:1). The browser outline is removed (outline: 0), but only because this ring replaces it. - Contrast: the
#6b7280titles on#e0e5ecmeasure 3.8:1 and the violet value#8b5cf63.3:1, at 12.5 px: below the 4.5:1 threshold. Switch them to#374151(8.1:1) and#6d28d9(5.6:1), as in the scenes. The violet fill stands out at 3.3:1 on the track; the handle only exists through its shadows (1.5:1). - Touch: the grab area is 24 px tall across the full width (WCAG 2.5.8 asks for at least 24 × 24 px). On a handheld screen, 32 to 44 px is more comfortable: enlarge the input and the handle together (see Customize).
Browser compatibility
CSS: appearance: none, engine-specific track and handle pseudo-elements (-webkit- for Chrome, Edge and Safari, -moz- for Firefox, in separate rules: an unknown selector would invalidate the whole rule), calc() with a CSS variable in background-size, :focus-visible. JavaScript: an input listener, style.setProperty. Measured in Chromium, Firefox and WebKit 26 (Playwright): identical rendering (10 px track, centered handle), drag, touch and keyboard working, clicks up to 11 px below the track's center registered in all three.
Without JavaScript, the sliders stay usable (they are native inputs) but the displayed value no longer follows and the fill stays at 50%, the default of --neu-p. The versions given are those of :focus-visible; before them, everything works except the keyboard focus ring.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="neu-slider-scene">
<label class="neu-slider-group">
<span class="neu-slider-header">
<span class="neu-slider-title">Volume</span>
<span class="neu-slider-value" aria-hidden="true">75%</span>
</span>
<input type="range" class="neu-slider" min="0" max="100" value="75">
</label>
<!-- second slider: same structure -->
</div>
The full HTML + CSS + JS of Neu Slider, ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Bounds and step (HTML — min, max, step, value) |
0 to 100, step 1 | Everything is set on the input: min="1" max="5" step="1" for five hood speeds. The JavaScript computes the ratio from min and max; the fill follows. |
Displayed value (JS — out.textContent = s.value + '%') |
value followed by “%” | Replace with your unit (' dB', ' K') or a label (['Low', 'Medium', 'High'][s.value]). Add s.setAttribute('aria-valuetext', …) with the same text. |
Fill color (CSS — gradient in the background of both tracks) |
#8b5cf6 → #a78bfa | Change it in ::-webkit-slider-runnable-track AND ::-moz-range-track, keeping the size calc(12px + (100% - 24px) * var(--neu-p, .5)). A modifier class gives one color per slider (like the amber in scene 3). |
Grab size (CSS — input height: 24px, 24 px handle, margin-top: -7px) |
24 px tall | For 32 px: input and handle at 32 px, margin-top: -11px on the WebKit handle (half of 10 − 32), and replace the 12px / 24px of the fill formula with 16px / 32px. |
Material tint (CSS — #e0e5ec, shadows #b8bec7 / #fff) |
light bluish gray | Track, handle and the .neu-slider-scene surface share one tint: change them together. |
FAQ
slider.addEventListener('input', () => update(slider.value)) to follow the drag, change for the final value. In a form, give the input a name: the value is sent with the other fields.