Tailwind v4MIT licensed

Tailwind Liquid Glass Range Slider

A heavy glass lens sliding across a numeric scale, magnifying the value beneath it. The native range input stays on top and fully transparent so keyboard, pointer and screen-reader behaviour are untouched; every glass layer under it is decoration positioned from a single --v custom property.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section id="lgt-13-root" class="lgt-13 grid w-full min-h-svh place-items-center px-6 py-15 font-lgt13-sans [background:radial-gradient(circle_at_30%_30%,#1c3050,#0d1424)]" aria-label="Tailwind liquid glass range slider">
  <div id="lgt-13-wrap" style="--v:40" class="group relative h-30 w-[min(440px,90vw)]">
    <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-8.5 flex justify-between text-[26px] font-extrabold text-white/14">
      <span>0</span><span>25</span><span>50</span><span>75</span><span>100</span>
    </div>

    <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-13.5 h-2.5 overflow-hidden rounded-full border border-white/20 bg-white/10 backdrop-blur-[6px] shadow-[inset_0_2px_6px_rgba(0,0,0,.5)]">
      <div class="absolute inset-y-0 left-0 [width:calc(var(--v)*1%)] bg-linear-to-r from-lgt13-accent/60 to-lgt13-accent shadow-[0_0_16px_var(--color-lgt13-accent)]"></div>
    </div>

    <div aria-hidden="true" class="pointer-events-none absolute top-[59px] grid size-14 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-white/60 bg-white/14 backdrop-blur-[3px] backdrop-saturate-160 backdrop-brightness-110 shadow-[inset_0_2px_6px_rgba(255,255,255,.5),inset_0_-6px_12px_rgba(0,0,0,.35),0_10px_24px_-8px_rgba(0,0,0,.6)] [left:calc(var(--v)*1%)] transition-shadow duration-200 group-has-[:focus-visible]:shadow-[0_0_0_3px_#fff,inset_0_2px_6px_rgba(255,255,255,.5)] motion-reduce:transition-none">
      <span id="lgt-13-mag" class="scale-115 text-2xl font-black text-white [text-shadow:0_1px_4px_rgba(0,0,0,.5)]">40</span>
    </div>

    <input id="lgt-13-input" type="range" min="0" max="100" value="40" aria-label="Quality" class="absolute -left-2 top-11 h-10 w-[calc(100%+16px)] cursor-grab appearance-none bg-transparent opacity-0 active:cursor-grabbing">
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-lgt13-accent: #4fd1e0;
  --color-lgt13-accent: oklch(0.7 0.17 200);
  --font-lgt13-sans: 'Manrope', system-ui, sans-serif;
}
/* The invisible input still needs a large hit area on both engines. */

.lgt-13 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 56px;
  height: 56px;
}

.lgt-13 input[type="range"]::-moz-range-thumb {
  width: 56px;
  height: 56px;
  border: none;
  background: transparent;
}
(() => {
  const root = document.querySelector('#lgt-13-root');
  const input = root.querySelector('#lgt-13-input');
  const wrap = root.querySelector('#lgt-13-wrap');
  const mag = root.querySelector('#lgt-13-mag');
  const sync = () => { wrap.style.setProperty('--v', input.value); mag.textContent = input.value; };
  input.addEventListener('input', sync);
  sync();
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-lgt13-accent: #4fd1e0;
  --color-lgt13-accent: oklch(0.7 0.17 200);
  --font-lgt13-sans: 'Manrope', system-ui, sans-serif;
}
/* The invisible input still needs a large hit area on both engines. */

.lgt-13 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 56px;
  height: 56px;
}

.lgt-13 input[type="range"]::-moz-range-thumb {
  width: 56px;
  height: 56px;
  border: none;
  background: transparent;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The native input stays on top at opacity-0, so keyboard, touch, drag and value announcements are the browser's job, not the script's.
  • group-has-[:focus-visible]: rings the visible lens when the invisible input takes focus — this replaces the CSS version's JS focus and blur handlers entirely.
  • One --v variable positions the lens and sizes the fill through arbitrary calc() utilities.
  • The script is four lines: it syncs a variable and a label, and nothing else.
  • The oversized vendor thumb rules stay in CSS because they exist purely to give the transparent input a 56px drag target.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Liquid Glass Range Slider
Source: https://codefronts.com/design-styles/tailwind-liquid-glass-effects/tailwind-liquid-glass-range-slider/

A heavy glass lens sliding across a numeric scale, magnifying the value beneath it. The native range input stays on top and fully transparent so keyboard, pointer and screen-reader behaviour are untouched; every glass layer under it is decoration positioned from a single --v custom property.
## HTML
```html
<section id="lgt-13-root" class="lgt-13 grid w-full min-h-svh place-items-center px-6 py-15 font-lgt13-sans [background:radial-gradient(circle_at_30%_30%,#1c3050,#0d1424)]" aria-label="Tailwind liquid glass range slider">
  <div id="lgt-13-wrap" style="--v:40" class="group relative h-30 w-[min(440px,90vw)]">
    <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-8.5 flex justify-between text-[26px] font-extrabold text-white/14">
      <span>0</span><span>25</span><span>50</span><span>75</span><span>100</span>
    </div>

    <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-13.5 h-2.5 overflow-hidden rounded-full border border-white/20 bg-white/10 backdrop-blur-[6px] shadow-[inset_0_2px_6px_rgba(0,0,0,.5)]">
      <div class="absolute inset-y-0 left-0 [width:calc(var(--v)*1%)] bg-linear-to-r from-lgt13-accent/60 to-lgt13-accent shadow-[0_0_16px_var(--color-lgt13-accent)]"></div>
    </div>

    <div aria-hidden="true" class="pointer-events-none absolute top-[59px] grid size-14 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border border-white/60 bg-white/14 backdrop-blur-[3px] backdrop-saturate-160 backdrop-brightness-110 shadow-[inset_0_2px_6px_rgba(255,255,255,.5),inset_0_-6px_12px_rgba(0,0,0,.35),0_10px_24px_-8px_rgba(0,0,0,.6)] [left:calc(var(--v)*1%)] transition-shadow duration-200 group-has-[:focus-visible]:shadow-[0_0_0_3px_#fff,inset_0_2px_6px_rgba(255,255,255,.5)] motion-reduce:transition-none">
      <span id="lgt-13-mag" class="scale-115 text-2xl font-black text-white [text-shadow:0_1px_4px_rgba(0,0,0,.5)]">40</span>
    </div>

    <input id="lgt-13-input" type="range" min="0" max="100" value="40" aria-label="Quality" class="absolute -left-2 top-11 h-10 w-[calc(100%+16px)] cursor-grab appearance-none bg-transparent opacity-0 active:cursor-grabbing">
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-lgt13-accent: #4fd1e0;
  --color-lgt13-accent: oklch(0.7 0.17 200);
  --font-lgt13-sans: 'Manrope', system-ui, sans-serif;
}
/* The invisible input still needs a large hit area on both engines. */

.lgt-13 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 56px;
  height: 56px;
}

.lgt-13 input[type="range"]::-moz-range-thumb {
  width: 56px;
  height: 56px;
  border: none;
  background: transparent;
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('#lgt-13-root');
  const input = root.querySelector('#lgt-13-input');
  const wrap = root.querySelector('#lgt-13-wrap');
  const mag = root.querySelector('#lgt-13-mag');
  const sync = () => { wrap.style.setProperty('--v', input.value); mag.textContent = input.value; };
  input.addEventListener('input', sync);
  sync();
})();
```

How this works

The accessible control is the one you cannot see. A real range input sits on top at opacity-0. It handles dragging, arrow keys, Home/End, page keys and the screen reader's value announcements. Everything glass underneath is painted from its value:

<!-- fill -->   class="[width:calc(var(--v)*1%)]"
<!-- lens -->   class="[left:calc(var(--v)*1%)] -translate-x-1/2"

has-[:focus-visible]: solves the focus-ring problem. The focusable element is invisible, so its own ring would be invisible too. The wrapper asks 'does something inside me have visible focus?' and rings the lens instead — one variant replacing the JS focus/blur handlers the CSS version needed.

Optical thickness is two inset shadows. A bright inset at the top edge and a darker one at the bottom, plus a light backdrop brightness boost. That combination is what separates a glass lens from a white circle.

The magnified number is honest. It shows the live value at 1.15 scale over the dimmed scale beneath, so the lens appears to be magnifying the ruler it slides along.

Make it yours

  • Resize the lens and its zoom together — the disc diameter and the magnified number's scale should move as a pair or the illusion breaks.
  • Add step=&quot;5&quot; for tick snapping; the lens follows automatically because it reads the value.
  • Recolour the fill and its glow from --color-lgt13-accent.
  • Swap the magnified number for a value bubble above the lens if your scale is not numeric.
  • Widen the track and the scale together; the percentages keep everything aligned at any width.

Gotchas — read before shipping

  • Keep the native input on top and transparent. Hiding it and rebuilding drag with pointer events costs you keyboard support, screen-reader values and touch behaviour in one move.
  • opacity-0 is right here; hidden or sr-only would remove the pointer target that makes the slider draggable.
  • Every decorative layer needs pointer-events-none, or the lens will swallow the drag it is supposed to represent.
  • backdrop-filter on a small circular element is more expensive than it looks — keep the blur at 3px, not 15px.
  • Drive the lens from the input's value on every input event; do not try to compute it from pointer coordinates.
  • The invisible input still needs a visible focus affordance. Without the has-[:focus-visible]: ring, keyboard users are steering blind.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The native range input gives full accessibility everywhere; :has() (Chrome 105+, Safari 15.4+, Firefox 121+) drives the focus ring and Tailwind v4's sheet sets the floor. Without backdrop-filter the lens is a solid glass ball that still shows position and value.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…