Tailwind + JSMIT licensed

Tailwind CSS Neumorphic Volume Slider with Soft UI Shadows

Soft UI done honestly: one base colour, two opposing shadows, and nothing else. The port reads well because neumorphism is entirely a shadow language — shadow-[10px_10px_30px_var(--color-trs02-dark),-10px_-10px_30px_var(--color-trs02-light)] states the extrusion inline, and the pressed state is the same utility with inset and smaller offsets.

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 class="trs-02 w-full min-h-svh grid place-items-center bg-trs02-base font-trs02-sans px-5 py-10" id="trs-02-root" style="--v:.6">
  <div class="w-[min(400px,100%)] rounded-[28px] bg-trs02-base p-9 shadow-[10px_10px_30px_var(--color-trs02-dark),-10px_-10px_30px_var(--color-trs02-light)]">
    <header class="flex items-center justify-between gap-3 mb-8">
      <button type="button" aria-label="Previous track" class="grid place-items-center size-13 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 hover:text-trs02-acc active:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]"><polyline points="15 18 9 12 15 6"/></svg>
      </button>
      <h3 class="text-[15px] font-bold tracking-wide text-trs02-ink m-0">Volume control</h3>
      <button type="button" aria-label="Next track" class="grid place-items-center size-13 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 hover:text-trs02-acc active:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]"><polyline points="9 18 15 12 9 6"/></svg>
      </button>
    </header>

    <div class="rounded-2xl bg-trs02-base px-5 py-6 mb-7 shadow-[inset_5px_5px_14px_var(--color-trs02-dark),inset_-5px_-5px_14px_var(--color-trs02-light)]">
      <input type="range" min="0" max="100" value="60" id="trs-02-vol" aria-label="Volume"
             class="block w-full h-2 appearance-none bg-transparent outline-none cursor-pointer bg-[linear-gradient(90deg,var(--color-trs02-acc)_calc(var(--v)*100%),var(--color-trs02-dark)_calc(var(--v)*100%))] rounded-full focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-trs02-acc">
    </div>

    <div class="flex items-end justify-between gap-4">
      <div class="flex items-baseline gap-1">
        <span class="text-[42px] leading-none font-black tracking-tight text-trs02-ink tabular-nums" id="trs-02-num">60</span>
        <span class="text-base font-medium text-trs02-mut">%</span>
      </div>
      <div class="flex items-end gap-[5px]" id="trs-02-meter" aria-hidden="true"></div>
      <button type="button" id="trs-02-mute" aria-pressed="false" class="grid place-items-center min-h-11 min-w-11 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 aria-pressed:text-trs02-acc aria-pressed:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <span class="sr-only">Mute</span>
        <svg viewBox="0 0 24 24" class="size-5 fill-current"><polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      </button>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-trs02-base: #e0e5ec;
  --color-trs02-base: oklch(0.91 0.01 255);
  --color-trs02-dark: #b3bac6;
  --color-trs02-dark: oklch(0.79 0.018 258);
  --color-trs02-light: #ffffff;
  --color-trs02-light: oklch(1 0 0);
  --color-trs02-acc: #5a4ff3;
  --color-trs02-acc: oklch(0.53 0.21 280);
  --color-trs02-ink: #2f3a4c;
  --color-trs02-ink: oklch(0.36 0.03 262);
  --color-trs02-mut: #5c6b83;
  --color-trs02-mut: oklch(0.5 0.035 258);
  --font-trs02-sans: 'Outfit', system-ui, sans-serif;
}

@property --v {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.6;
}

.trs-02 {
  width: 100%;
  min-height: 100svh;
}
/* Thumb: the extruded knob, and the only place the neumorphic shadow pair
   cannot be written as a utility. */

.trs-02 input[type="range"]::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 9999px;
  background: transparent;
}

.trs-02 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 32px;
  height: 32px;
  margin-top: -12px;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
  transition: box-shadow .2s;
}

.trs-02 input[type="range"]:active::-webkit-slider-thumb {
  cursor: grabbing;
  box-shadow: 3px 3px 8px var(--color-trs02-dark), -3px -3px 8px var(--color-trs02-light), 0 0 0 5px color-mix(in oklab, var(--color-trs02-acc) 28%, transparent);
}

.trs-02 input[type="range"]::-moz-range-track {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-dark);
}

.trs-02 input[type="range"]::-moz-range-progress {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-acc);
}

.trs-02 input[type="range"]::-moz-range-thumb {
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
}

@media (prefers-reduced-motion: reduce) {
  .trs-02 input[type="range"]::-webkit-slider-thumb {
    transition: none;
  }
}
(function(){
  var root = document.getElementById('trs-02-root');
  var inp = document.getElementById('trs-02-vol');
  var num = document.getElementById('trs-02-num');
  var meter = document.getElementById('trs-02-meter');
  var mute = document.getElementById('trs-02-mute');
  var heights = [10,14,18,22,28,34,28,22,18,14,10,8];
  var last = 60;

  heights.forEach(function(h){
    var b = document.createElement('span');
    b.className = 'w-1.5 rounded-full bg-trs02-dark transition-colors duration-300 data-[on=true]:bg-trs02-acc motion-reduce:transition-none';
    b.style.height = h + 'px';
    b.dataset.on = 'false';
    meter.appendChild(b);
  });
  var bars = Array.prototype.slice.call(meter.children);

  function update(){
    var v = +inp.value;
    root.style.setProperty('--v', v / 100);
    num.textContent = v;
    inp.setAttribute('aria-valuetext', v === 0 ? 'Muted' : v + ' percent');
    var on = Math.round(v / 100 * bars.length);
    bars.forEach(function(b, i){ b.dataset.on = i < on ? 'true' : 'false'; });
    mute.setAttribute('aria-pressed', String(v === 0));
  }

  inp.addEventListener('input', function(){ if (+inp.value > 0) last = +inp.value; update(); });
  mute.addEventListener('click', function(){
    inp.value = +inp.value === 0 ? (last || 60) : 0;
    update();
  });
  update();
})();

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-trs02-base: #e0e5ec;
  --color-trs02-base: oklch(0.91 0.01 255);
  --color-trs02-dark: #b3bac6;
  --color-trs02-dark: oklch(0.79 0.018 258);
  --color-trs02-light: #ffffff;
  --color-trs02-light: oklch(1 0 0);
  --color-trs02-acc: #5a4ff3;
  --color-trs02-acc: oklch(0.53 0.21 280);
  --color-trs02-ink: #2f3a4c;
  --color-trs02-ink: oklch(0.36 0.03 262);
  --color-trs02-mut: #5c6b83;
  --color-trs02-mut: oklch(0.5 0.035 258);
  --font-trs02-sans: 'Outfit', system-ui, sans-serif;
}

@property --v {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.6;
}

.trs-02 {
  width: 100%;
  min-height: 100svh;
}
/* Thumb: the extruded knob, and the only place the neumorphic shadow pair
   cannot be written as a utility. */

.trs-02 input[type="range"]::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 9999px;
  background: transparent;
}

.trs-02 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 32px;
  height: 32px;
  margin-top: -12px;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
  transition: box-shadow .2s;
}

.trs-02 input[type="range"]:active::-webkit-slider-thumb {
  cursor: grabbing;
  box-shadow: 3px 3px 8px var(--color-trs02-dark), -3px -3px 8px var(--color-trs02-light), 0 0 0 5px color-mix(in oklab, var(--color-trs02-acc) 28%, transparent);
}

.trs-02 input[type="range"]::-moz-range-track {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-dark);
}

.trs-02 input[type="range"]::-moz-range-progress {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-acc);
}

.trs-02 input[type="range"]::-moz-range-thumb {
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
}

@media (prefers-reduced-motion: reduce) {
  .trs-02 input[type="range"]::-webkit-slider-thumb {
    transition: none;
  }
}
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=Outfit:wght@400;500;600;700;900&display=swap">

What changed in the port

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

  • Every neumorphic surface is an arbitrary shadow-[…] pair, and the pressed variant is the same utility with inset — the aesthetic now reads off the element instead of a stylesheet three screens away.
  • The fill stopped being a div. A two-stop gradient with its edge at calc(var(--v)*100%) lives in the input's class list; Firefox keeps ::-moz-range-progress.
  • UI upgrade: a working mute button with aria-pressed and value restore, plus an aria-valuetext that announces "Muted" rather than "0".
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 CSS Neumorphic Volume Slider with Soft UI Shadows
Source: https://codefronts.com/components/tailwind-css-range-sliders/tailwind-neumorphic-volume-slider-soft-ui/

Soft UI done honestly: one base colour, two opposing shadows, and nothing else. The port reads well because neumorphism is entirely a shadow language — shadow-[10px_10px_30px_var(--color-trs02-dark),-10px_-10px_30px_var(--color-trs02-light)] states the extrusion inline, and the pressed state is the same utility with inset and smaller offsets.
## HTML
```html
<section class="trs-02 w-full min-h-svh grid place-items-center bg-trs02-base font-trs02-sans px-5 py-10" id="trs-02-root" style="--v:.6">
  <div class="w-[min(400px,100%)] rounded-[28px] bg-trs02-base p-9 shadow-[10px_10px_30px_var(--color-trs02-dark),-10px_-10px_30px_var(--color-trs02-light)]">
    <header class="flex items-center justify-between gap-3 mb-8">
      <button type="button" aria-label="Previous track" class="grid place-items-center size-13 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 hover:text-trs02-acc active:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]"><polyline points="15 18 9 12 15 6"/></svg>
      </button>
      <h3 class="text-[15px] font-bold tracking-wide text-trs02-ink m-0">Volume control</h3>
      <button type="button" aria-label="Next track" class="grid place-items-center size-13 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 hover:text-trs02-acc active:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]"><polyline points="9 18 15 12 9 6"/></svg>
      </button>
    </header>

    <div class="rounded-2xl bg-trs02-base px-5 py-6 mb-7 shadow-[inset_5px_5px_14px_var(--color-trs02-dark),inset_-5px_-5px_14px_var(--color-trs02-light)]">
      <input type="range" min="0" max="100" value="60" id="trs-02-vol" aria-label="Volume"
             class="block w-full h-2 appearance-none bg-transparent outline-none cursor-pointer bg-[linear-gradient(90deg,var(--color-trs02-acc)_calc(var(--v)*100%),var(--color-trs02-dark)_calc(var(--v)*100%))] rounded-full focus-visible:outline-2 focus-visible:outline-offset-8 focus-visible:outline-trs02-acc">
    </div>

    <div class="flex items-end justify-between gap-4">
      <div class="flex items-baseline gap-1">
        <span class="text-[42px] leading-none font-black tracking-tight text-trs02-ink tabular-nums" id="trs-02-num">60</span>
        <span class="text-base font-medium text-trs02-mut">%</span>
      </div>
      <div class="flex items-end gap-[5px]" id="trs-02-meter" aria-hidden="true"></div>
      <button type="button" id="trs-02-mute" aria-pressed="false" class="grid place-items-center min-h-11 min-w-11 rounded-2xl bg-trs02-base text-trs02-mut shadow-[5px_5px_14px_var(--color-trs02-dark),-5px_-5px_14px_var(--color-trs02-light)] transition-shadow duration-200 aria-pressed:text-trs02-acc aria-pressed:shadow-[inset_4px_4px_10px_var(--color-trs02-dark),inset_-4px_-4px_10px_var(--color-trs02-light)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-trs02-acc">
        <span class="sr-only">Mute</span>
        <svg viewBox="0 0 24 24" class="size-5 fill-current"><polygon points="11,5 6,9 2,9 2,15 6,15 11,19"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
      </button>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-trs02-base: #e0e5ec;
  --color-trs02-base: oklch(0.91 0.01 255);
  --color-trs02-dark: #b3bac6;
  --color-trs02-dark: oklch(0.79 0.018 258);
  --color-trs02-light: #ffffff;
  --color-trs02-light: oklch(1 0 0);
  --color-trs02-acc: #5a4ff3;
  --color-trs02-acc: oklch(0.53 0.21 280);
  --color-trs02-ink: #2f3a4c;
  --color-trs02-ink: oklch(0.36 0.03 262);
  --color-trs02-mut: #5c6b83;
  --color-trs02-mut: oklch(0.5 0.035 258);
  --font-trs02-sans: 'Outfit', system-ui, sans-serif;
}

@property --v {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.6;
}

.trs-02 {
  width: 100%;
  min-height: 100svh;
}
/* Thumb: the extruded knob, and the only place the neumorphic shadow pair
   cannot be written as a utility. */

.trs-02 input[type="range"]::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 9999px;
  background: transparent;
}

.trs-02 input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 32px;
  height: 32px;
  margin-top: -12px;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
  transition: box-shadow .2s;
}

.trs-02 input[type="range"]:active::-webkit-slider-thumb {
  cursor: grabbing;
  box-shadow: 3px 3px 8px var(--color-trs02-dark), -3px -3px 8px var(--color-trs02-light), 0 0 0 5px color-mix(in oklab, var(--color-trs02-acc) 28%, transparent);
}

.trs-02 input[type="range"]::-moz-range-track {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-dark);
}

.trs-02 input[type="range"]::-moz-range-progress {
  height: 8px;
  border-radius: 9999px;
  background: var(--color-trs02-acc);
}

.trs-02 input[type="range"]::-moz-range-thumb {
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 9999px;
  background: var(--color-trs02-base);
  cursor: grab;
  box-shadow: 5px 5px 14px var(--color-trs02-dark), -5px -5px 14px var(--color-trs02-light);
}

@media (prefers-reduced-motion: reduce) {
  .trs-02 input[type="range"]::-webkit-slider-thumb {
    transition: none;
  }
}
```

## JavaScript
```js
(function(){
  var root = document.getElementById('trs-02-root');
  var inp = document.getElementById('trs-02-vol');
  var num = document.getElementById('trs-02-num');
  var meter = document.getElementById('trs-02-meter');
  var mute = document.getElementById('trs-02-mute');
  var heights = [10,14,18,22,28,34,28,22,18,14,10,8];
  var last = 60;

  heights.forEach(function(h){
    var b = document.createElement('span');
    b.className = 'w-1.5 rounded-full bg-trs02-dark transition-colors duration-300 data-[on=true]:bg-trs02-acc motion-reduce:transition-none';
    b.style.height = h + 'px';
    b.dataset.on = 'false';
    meter.appendChild(b);
  });
  var bars = Array.prototype.slice.call(meter.children);

  function update(){
    var v = +inp.value;
    root.style.setProperty('--v', v / 100);
    num.textContent = v;
    inp.setAttribute('aria-valuetext', v === 0 ? 'Muted' : v + ' percent');
    var on = Math.round(v / 100 * bars.length);
    bars.forEach(function(b, i){ b.dataset.on = i < on ? 'true' : 'false'; });
    mute.setAttribute('aria-pressed', String(v === 0));
  }

  inp.addEventListener('input', function(){ if (+inp.value > 0) last = +inp.value; update(); });
  mute.addEventListener('click', function(){
    inp.value = +inp.value === 0 ? (last || 60) : 0;
    update();
  });
  update();
})();
```

How this works

The groove and the knob are the same technique, inverted. The panel pushes out (shadow-[10px_10px_30px_dark,-10px_-10px_30px_light]), the track pulls in (shadow-[inset_5px_5px_14px_dark,inset_-5px_-5px_14px_light]), and the thumb — which must stay in CSS — carries the outward pair plus an accent halo while active.

The fill is a background, not a div. A two-stop gradient with its hard edge at calc(var(--v)*100%) paints the filled portion directly on the input, and ::-moz-range-progress covers Firefox. One property write, no layout.

Mute is the upgrade. The CSS original listed it as an exercise; here it is wired, keyboard-reachable, aria-pressed-labelled, and restores the previous level rather than jumping to a default.

Make it yours

  • Palette: --color-trs02-acc, with --color-trs02-dark/-light kept as a matched pair against the base — neumorphism breaks the moment they drift.
  • Extrusion depth: scale the shadow offsets together (10px/30px → 6px/18px for a flatter surface).
  • Meter resolution: the heights array length is the bar count; the flex row redistributes automatically.

Gotchas — read before shipping

  • Neumorphism needs the component's background to equal the shadow base. The root paints bg-trs02-base itself rather than inheriting the host page.
  • ::-moz-range-progress is the only way Firefox shows a filled portion — the gradient trick is WebKit/Blink only, so both ship.
  • Soft UI is low-contrast by nature. The readout, labels and button text here are all ≥4.5:1 against the base; do not dim them further to "match the style".

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

The gradient split-fill needs ::-webkit-slider-runnable-track; Firefox uses the ::-moz-range-progress rule shipped alongside it.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…