12 / 17
All 17 in 17 Tailwind CSS Range Sliders
- 01Tailwind CSS Dual-Handle Price Range Slider with Live Product Count
- 02Tailwind CSS Neumorphic Volume Slider with Soft UI Shadows
- 03Tailwind CSS Neon Glow Range Slider for Dark Mode UI
- 04Tailwind CSS Emoji Mood Slider for Feedback & CSAT Surveys
- 05Tailwind CSS Multi-Step Pricing Slider with Snapping Milestones
- 06Tailwind CSS Video Player Seekbar with Hover Scrub & Chapters
- 07Tailwind CSS Vertical Range Sliders — 10-Band Audio Equalizer
- 08Tailwind CSS Circular Dial Range Input — Smart Thermostat UI
- 09Tailwind CSS Gradient Range Slider with Progress Milestones
- 10Tailwind CSS Range Slider with Pop-Up Value Bubble (Loan Calculator)
- 11Tailwind CSS Brutalist Range Slider with Thick Block Track
- 12Tailwind CSS Password Strength Meter with Range Slider Control
- 13Tailwind CSS Centre-Origin Balance Slider for Audio Pan Control
- 14Tailwind CSS Retro 8-Bit Pixel Art Range Slider (Game HP Bar)
- 15Tailwind CSS Isometric 3D Range Slider with Perspective Plane
- 16Tailwind CSS Glassmorphism Range Sliders — RGBA Colour Mixer
- 17Tailwind CSS Liquid Glass Slider with Gooey Morphing Value Bubble
Tailwind CSS Password Strength Meter with Range Slider Control
Four strength zones — weak, fair, good, strong — driving a shield, four segments, the fill, the numeral and a live checklist. The port replaces eight inline style writes per change with one --tone property, and the slider is now linked to a real password field: type, and the meter follows.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="trs-12 w-full min-h-svh grid place-items-center bg-trs12-bg font-trs12-sans px-5 py-10" id="trs-12-root" style="--v:.143;--tone:var(--color-trs12-fair)">
<div class="w-[min(430px,100%)] rounded-3xl bg-trs12-card border border-trs12-edge p-9 shadow-[0_24px_70px_-28px_oklch(0_0_0/0.75)]">
<div class="grid place-items-center size-14 rounded-2xl text-[28px] mb-5 bg-[color-mix(in_oklab,var(--tone)_18%,transparent)] shadow-[0_0_28px_-6px_var(--tone)]" id="trs-12-shield" aria-hidden="true">🔒</div>
<h3 class="text-xl font-bold text-trs12-ink m-0">Password strength</h3>
<p class="text-[13px] text-trs12-mut mt-1 mb-6 m-0">Type a password, or drag the character count</p>
<label class="block mb-6">
<span class="sr-only">Password</span>
<input type="password" id="trs-12-pw" autocomplete="new-password" placeholder="Try typing here…"
class="w-full min-h-11 rounded-xl bg-trs12-well border border-trs12-edge px-3.5 font-trs12-mono text-sm text-trs12-ink placeholder:text-trs12-mut outline-none transition-colors duration-200 focus-visible:border-(--tone) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone)">
</label>
<div class="flex items-center justify-between gap-3 mb-3">
<span class="text-[13px] text-trs12-mut">Strength level</span>
<span class="rounded-full px-2.5 py-1 text-xs font-bold transition-[background-color,color] duration-400 motion-reduce:transition-none" id="trs-12-badge" aria-live="polite">Fair</span>
</div>
<div class="flex gap-1 mb-6" id="trs-12-segs" aria-hidden="true">
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
</div>
<div class="relative h-12 flex items-center mb-5">
<div class="absolute inset-x-0 h-2.5 rounded-full bg-trs12-edge"></div>
<div class="absolute left-0 h-2.5 rounded-full w-[calc(var(--v)*100%)] bg-(--tone)"></div>
<input type="range" min="4" max="32" value="8" id="trs-12-inp" aria-label="Password length"
class="absolute inset-0 w-full h-12 appearance-none bg-transparent outline-none cursor-pointer z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone) rounded-full">
</div>
<div class="flex items-baseline gap-2 mb-6">
<span class="text-[64px] leading-none font-bold tracking-tighter tabular-nums text-(--tone)" id="trs-12-num">8</span>
<span class="text-[13px] text-trs12-mut">characters</span>
</div>
<ul class="flex flex-col gap-2 m-0 p-0 list-none" id="trs-12-rules"></ul>
</div>
</section><section class="trs-12 w-full min-h-svh grid place-items-center bg-trs12-bg font-trs12-sans px-5 py-10" id="trs-12-root" style="--v:.143;--tone:var(--color-trs12-fair)">
<div class="w-[min(430px,100%)] rounded-3xl bg-trs12-card border border-trs12-edge p-9 shadow-[0_24px_70px_-28px_oklch(0_0_0/0.75)]">
<div class="grid place-items-center size-14 rounded-2xl text-[28px] mb-5 bg-[color-mix(in_oklab,var(--tone)_18%,transparent)] shadow-[0_0_28px_-6px_var(--tone)]" id="trs-12-shield" aria-hidden="true">🔒</div>
<h3 class="text-xl font-bold text-trs12-ink m-0">Password strength</h3>
<p class="text-[13px] text-trs12-mut mt-1 mb-6 m-0">Type a password, or drag the character count</p>
<label class="block mb-6">
<span class="sr-only">Password</span>
<input type="password" id="trs-12-pw" autocomplete="new-password" placeholder="Try typing here…"
class="w-full min-h-11 rounded-xl bg-trs12-well border border-trs12-edge px-3.5 font-trs12-mono text-sm text-trs12-ink placeholder:text-trs12-mut outline-none transition-colors duration-200 focus-visible:border-(--tone) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone)">
</label>
<div class="flex items-center justify-between gap-3 mb-3">
<span class="text-[13px] text-trs12-mut">Strength level</span>
<span class="rounded-full px-2.5 py-1 text-xs font-bold transition-[background-color,color] duration-400 motion-reduce:transition-none" id="trs-12-badge" aria-live="polite">Fair</span>
</div>
<div class="flex gap-1 mb-6" id="trs-12-segs" aria-hidden="true">
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
</div>
<div class="relative h-12 flex items-center mb-5">
<div class="absolute inset-x-0 h-2.5 rounded-full bg-trs12-edge"></div>
<div class="absolute left-0 h-2.5 rounded-full w-[calc(var(--v)*100%)] bg-(--tone)"></div>
<input type="range" min="4" max="32" value="8" id="trs-12-inp" aria-label="Password length"
class="absolute inset-0 w-full h-12 appearance-none bg-transparent outline-none cursor-pointer z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone) rounded-full">
</div>
<div class="flex items-baseline gap-2 mb-6">
<span class="text-[64px] leading-none font-bold tracking-tighter tabular-nums text-(--tone)" id="trs-12-num">8</span>
<span class="text-[13px] text-trs12-mut">characters</span>
</div>
<ul class="flex flex-col gap-2 m-0 p-0 list-none" id="trs-12-rules"></ul>
</div>
</section>@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}(function(){
var root = document.getElementById('trs-12-root');
var inp = document.getElementById('trs-12-inp');
var pw = document.getElementById('trs-12-pw');
var num = document.getElementById('trs-12-num');
var badge = document.getElementById('trs-12-badge');
var shield = document.getElementById('trs-12-shield');
var segs = Array.prototype.slice.call(document.getElementById('trs-12-segs').children);
var rulesEl = document.getElementById('trs-12-rules');
var LEVELS = [
{ max: 7, tone: 'var(--color-trs12-weak)', label: 'Weak', badgeBg: 'oklch(0.95 0.03 25)', badgeInk: 'oklch(0.4 0.16 25)', icon: '🔓', segs: 1 },
{ max: 11, tone: 'var(--color-trs12-fair)', label: 'Fair', badgeBg: 'oklch(0.96 0.04 85)', badgeInk: 'oklch(0.45 0.13 70)', icon: '🔒', segs: 2 },
{ max: 19, tone: 'var(--color-trs12-good)', label: 'Good', badgeBg: 'oklch(0.95 0.04 165)', badgeInk: 'oklch(0.42 0.12 165)',icon: '🛡️', segs: 3 },
{ max: 99, tone: 'var(--color-trs12-strong)', label: 'Strong', badgeBg: 'oklch(0.95 0.04 205)', badgeInk: 'oklch(0.42 0.1 205)', icon: '🔐', segs: 4 }
];
var RULES = [[8, '8+ characters for basic security'], [12, '12+ characters recommended'], [20, '20+ characters for maximum security']];
var lastLabel = '';
RULES.forEach(function(r){
var li = document.createElement('li');
li.dataset.ok = 'false';
li.className = 'flex items-center gap-2.5 text-[13px] text-trs12-mut transition-colors duration-300 data-[ok=true]:text-trs12-ink motion-reduce:transition-none';
li.innerHTML = '<span class="grid place-items-center size-5 shrink-0 rounded-full bg-trs12-edge text-[11px] transition-colors duration-300 data-[ok=true]:bg-trs12-good motion-reduce:transition-none" data-ok="false" aria-hidden="true">✗</span><span>' + r[1] + '</span>';
rulesEl.appendChild(li);
});
var rows = Array.prototype.slice.call(rulesEl.children);
function level(v){ for (var i = 0; i < LEVELS.length; i++) if (v <= LEVELS[i].max) return LEVELS[i]; return LEVELS[3]; }
function update(){
var v = +inp.value, lv = level(v);
root.style.setProperty('--tone', lv.tone);
root.style.setProperty('--v', (v - +inp.min) / (+inp.max - +inp.min));
num.textContent = v;
shield.textContent = lv.icon;
if (lv.label !== lastLabel) {
badge.textContent = lv.label;
badge.style.backgroundColor = lv.badgeBg;
badge.style.color = lv.badgeInk;
lastLabel = lv.label;
}
segs.forEach(function(s, i){ s.dataset.on = i < lv.segs ? 'true' : 'false'; });
rows.forEach(function(row, i){
var ok = v >= RULES[i][0];
row.dataset.ok = ok ? 'true' : 'false';
var icon = row.firstElementChild;
icon.dataset.ok = ok ? 'true' : 'false';
icon.textContent = ok ? '✓' : '✗';
});
inp.setAttribute('aria-valuetext', lv.label + ', ' + v + ' characters');
}
inp.addEventListener('input', function(){ update(); });
pw.addEventListener('input', function(){
inp.value = Math.min(+inp.max, Math.max(+inp.min, pw.value.length));
update();
});
update();
})();(function(){
var root = document.getElementById('trs-12-root');
var inp = document.getElementById('trs-12-inp');
var pw = document.getElementById('trs-12-pw');
var num = document.getElementById('trs-12-num');
var badge = document.getElementById('trs-12-badge');
var shield = document.getElementById('trs-12-shield');
var segs = Array.prototype.slice.call(document.getElementById('trs-12-segs').children);
var rulesEl = document.getElementById('trs-12-rules');
var LEVELS = [
{ max: 7, tone: 'var(--color-trs12-weak)', label: 'Weak', badgeBg: 'oklch(0.95 0.03 25)', badgeInk: 'oklch(0.4 0.16 25)', icon: '🔓', segs: 1 },
{ max: 11, tone: 'var(--color-trs12-fair)', label: 'Fair', badgeBg: 'oklch(0.96 0.04 85)', badgeInk: 'oklch(0.45 0.13 70)', icon: '🔒', segs: 2 },
{ max: 19, tone: 'var(--color-trs12-good)', label: 'Good', badgeBg: 'oklch(0.95 0.04 165)', badgeInk: 'oklch(0.42 0.12 165)',icon: '🛡️', segs: 3 },
{ max: 99, tone: 'var(--color-trs12-strong)', label: 'Strong', badgeBg: 'oklch(0.95 0.04 205)', badgeInk: 'oklch(0.42 0.1 205)', icon: '🔐', segs: 4 }
];
var RULES = [[8, '8+ characters for basic security'], [12, '12+ characters recommended'], [20, '20+ characters for maximum security']];
var lastLabel = '';
RULES.forEach(function(r){
var li = document.createElement('li');
li.dataset.ok = 'false';
li.className = 'flex items-center gap-2.5 text-[13px] text-trs12-mut transition-colors duration-300 data-[ok=true]:text-trs12-ink motion-reduce:transition-none';
li.innerHTML = '<span class="grid place-items-center size-5 shrink-0 rounded-full bg-trs12-edge text-[11px] transition-colors duration-300 data-[ok=true]:bg-trs12-good motion-reduce:transition-none" data-ok="false" aria-hidden="true">✗</span><span>' + r[1] + '</span>';
rulesEl.appendChild(li);
});
var rows = Array.prototype.slice.call(rulesEl.children);
function level(v){ for (var i = 0; i < LEVELS.length; i++) if (v <= LEVELS[i].max) return LEVELS[i]; return LEVELS[3]; }
function update(){
var v = +inp.value, lv = level(v);
root.style.setProperty('--tone', lv.tone);
root.style.setProperty('--v', (v - +inp.min) / (+inp.max - +inp.min));
num.textContent = v;
shield.textContent = lv.icon;
if (lv.label !== lastLabel) {
badge.textContent = lv.label;
badge.style.backgroundColor = lv.badgeBg;
badge.style.color = lv.badgeInk;
lastLabel = lv.label;
}
segs.forEach(function(s, i){ s.dataset.on = i < lv.segs ? 'true' : 'false'; });
rows.forEach(function(row, i){
var ok = v >= RULES[i][0];
row.dataset.ok = ok ? 'true' : 'false';
var icon = row.firstElementChild;
icon.dataset.ok = ok ? 'true' : 'false';
icon.textContent = ok ? '✓' : '✗';
});
inp.setAttribute('aria-valuetext', lv.label + ', ' + v + ' characters');
}
inp.addEventListener('input', function(){ update(); });
pw.addEventListener('input', function(){
inp.value = Math.min(+inp.max, Math.max(+inp.min, pw.value.length));
update();
});
update();
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}<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=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap">
<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=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@500;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Eight per-change style writes across six nodes became one
--tonewrite consumed bybg-[var(--tone)],text-[var(--tone)]andborder-[var(--tone)]— the thumb reads it too, through the CSS shim. - The badge's slash-encoded colour pair (
'#fef2f2/#991b1b') is gone; each zone carries two named fields, so nothing is parsed at runtime. - UI upgrade: a real password field drives the meter, the strength badge is
aria-liveand only re-announces on zone change, and the checklist rows are a semantic<ul>. - Port constraint:
--toneis a registered<color>animated on the root, and the shield, fill, segments and numeral have no transitions of their own — the hue shift through weak, fair, good and strong depends on that split. - Port constraint: the four zone colours are consumed by CSS and JS rather than by utilities, so they are restated on
.trs-12to survive v4's@themetree-shaking, and every token-valued utility uses thebg-(--tone)shorthand.
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 Password Strength Meter with Range Slider Control
Source: https://codefronts.com/components/tailwind-css-range-sliders/tailwind-password-strength-meter-range-slider/
Four strength zones — weak, fair, good, strong — driving a shield, four segments, the fill, the numeral and a live checklist. The port replaces eight inline style writes per change with one --tone property, and the slider is now linked to a real password field: type, and the meter follows.
## HTML
```html
<section class="trs-12 w-full min-h-svh grid place-items-center bg-trs12-bg font-trs12-sans px-5 py-10" id="trs-12-root" style="--v:.143;--tone:var(--color-trs12-fair)">
<div class="w-[min(430px,100%)] rounded-3xl bg-trs12-card border border-trs12-edge p-9 shadow-[0_24px_70px_-28px_oklch(0_0_0/0.75)]">
<div class="grid place-items-center size-14 rounded-2xl text-[28px] mb-5 bg-[color-mix(in_oklab,var(--tone)_18%,transparent)] shadow-[0_0_28px_-6px_var(--tone)]" id="trs-12-shield" aria-hidden="true">🔒</div>
<h3 class="text-xl font-bold text-trs12-ink m-0">Password strength</h3>
<p class="text-[13px] text-trs12-mut mt-1 mb-6 m-0">Type a password, or drag the character count</p>
<label class="block mb-6">
<span class="sr-only">Password</span>
<input type="password" id="trs-12-pw" autocomplete="new-password" placeholder="Try typing here…"
class="w-full min-h-11 rounded-xl bg-trs12-well border border-trs12-edge px-3.5 font-trs12-mono text-sm text-trs12-ink placeholder:text-trs12-mut outline-none transition-colors duration-200 focus-visible:border-(--tone) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone)">
</label>
<div class="flex items-center justify-between gap-3 mb-3">
<span class="text-[13px] text-trs12-mut">Strength level</span>
<span class="rounded-full px-2.5 py-1 text-xs font-bold transition-[background-color,color] duration-400 motion-reduce:transition-none" id="trs-12-badge" aria-live="polite">Fair</span>
</div>
<div class="flex gap-1 mb-6" id="trs-12-segs" aria-hidden="true">
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
</div>
<div class="relative h-12 flex items-center mb-5">
<div class="absolute inset-x-0 h-2.5 rounded-full bg-trs12-edge"></div>
<div class="absolute left-0 h-2.5 rounded-full w-[calc(var(--v)*100%)] bg-(--tone)"></div>
<input type="range" min="4" max="32" value="8" id="trs-12-inp" aria-label="Password length"
class="absolute inset-0 w-full h-12 appearance-none bg-transparent outline-none cursor-pointer z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone) rounded-full">
</div>
<div class="flex items-baseline gap-2 mb-6">
<span class="text-[64px] leading-none font-bold tracking-tighter tabular-nums text-(--tone)" id="trs-12-num">8</span>
<span class="text-[13px] text-trs12-mut">characters</span>
</div>
<ul class="flex flex-col gap-2 m-0 p-0 list-none" id="trs-12-rules"></ul>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}
```
## JavaScript
```js
(function(){
var root = document.getElementById('trs-12-root');
var inp = document.getElementById('trs-12-inp');
var pw = document.getElementById('trs-12-pw');
var num = document.getElementById('trs-12-num');
var badge = document.getElementById('trs-12-badge');
var shield = document.getElementById('trs-12-shield');
var segs = Array.prototype.slice.call(document.getElementById('trs-12-segs').children);
var rulesEl = document.getElementById('trs-12-rules');
var LEVELS = [
{ max: 7, tone: 'var(--color-trs12-weak)', label: 'Weak', badgeBg: 'oklch(0.95 0.03 25)', badgeInk: 'oklch(0.4 0.16 25)', icon: '🔓', segs: 1 },
{ max: 11, tone: 'var(--color-trs12-fair)', label: 'Fair', badgeBg: 'oklch(0.96 0.04 85)', badgeInk: 'oklch(0.45 0.13 70)', icon: '🔒', segs: 2 },
{ max: 19, tone: 'var(--color-trs12-good)', label: 'Good', badgeBg: 'oklch(0.95 0.04 165)', badgeInk: 'oklch(0.42 0.12 165)',icon: '🛡️', segs: 3 },
{ max: 99, tone: 'var(--color-trs12-strong)', label: 'Strong', badgeBg: 'oklch(0.95 0.04 205)', badgeInk: 'oklch(0.42 0.1 205)', icon: '🔐', segs: 4 }
];
var RULES = [[8, '8+ characters for basic security'], [12, '12+ characters recommended'], [20, '20+ characters for maximum security']];
var lastLabel = '';
RULES.forEach(function(r){
var li = document.createElement('li');
li.dataset.ok = 'false';
li.className = 'flex items-center gap-2.5 text-[13px] text-trs12-mut transition-colors duration-300 data-[ok=true]:text-trs12-ink motion-reduce:transition-none';
li.innerHTML = '<span class="grid place-items-center size-5 shrink-0 rounded-full bg-trs12-edge text-[11px] transition-colors duration-300 data-[ok=true]:bg-trs12-good motion-reduce:transition-none" data-ok="false" aria-hidden="true">✗</span><span>' + r[1] + '</span>';
rulesEl.appendChild(li);
});
var rows = Array.prototype.slice.call(rulesEl.children);
function level(v){ for (var i = 0; i < LEVELS.length; i++) if (v <= LEVELS[i].max) return LEVELS[i]; return LEVELS[3]; }
function update(){
var v = +inp.value, lv = level(v);
root.style.setProperty('--tone', lv.tone);
root.style.setProperty('--v', (v - +inp.min) / (+inp.max - +inp.min));
num.textContent = v;
shield.textContent = lv.icon;
if (lv.label !== lastLabel) {
badge.textContent = lv.label;
badge.style.backgroundColor = lv.badgeBg;
badge.style.color = lv.badgeInk;
lastLabel = lv.label;
}
segs.forEach(function(s, i){ s.dataset.on = i < lv.segs ? 'true' : 'false'; });
rows.forEach(function(row, i){
var ok = v >= RULES[i][0];
row.dataset.ok = ok ? 'true' : 'false';
var icon = row.firstElementChild;
icon.dataset.ok = ok ? 'true' : 'false';
icon.textContent = ok ? '✓' : '✗';
});
inp.setAttribute('aria-valuetext', lv.label + ', ' + v + ' characters');
}
inp.addEventListener('input', function(){ update(); });
pw.addEventListener('input', function(){
inp.value = Math.min(+inp.max, Math.max(+inp.min, pw.value.length));
update();
});
update();
})();
```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 Password Strength Meter with Range Slider Control
Source: https://codefronts.com/components/tailwind-css-range-sliders/tailwind-password-strength-meter-range-slider/
Four strength zones — weak, fair, good, strong — driving a shield, four segments, the fill, the numeral and a live checklist. The port replaces eight inline style writes per change with one --tone property, and the slider is now linked to a real password field: type, and the meter follows.
## HTML
```html
<section class="trs-12 w-full min-h-svh grid place-items-center bg-trs12-bg font-trs12-sans px-5 py-10" id="trs-12-root" style="--v:.143;--tone:var(--color-trs12-fair)">
<div class="w-[min(430px,100%)] rounded-3xl bg-trs12-card border border-trs12-edge p-9 shadow-[0_24px_70px_-28px_oklch(0_0_0/0.75)]">
<div class="grid place-items-center size-14 rounded-2xl text-[28px] mb-5 bg-[color-mix(in_oklab,var(--tone)_18%,transparent)] shadow-[0_0_28px_-6px_var(--tone)]" id="trs-12-shield" aria-hidden="true">🔒</div>
<h3 class="text-xl font-bold text-trs12-ink m-0">Password strength</h3>
<p class="text-[13px] text-trs12-mut mt-1 mb-6 m-0">Type a password, or drag the character count</p>
<label class="block mb-6">
<span class="sr-only">Password</span>
<input type="password" id="trs-12-pw" autocomplete="new-password" placeholder="Try typing here…"
class="w-full min-h-11 rounded-xl bg-trs12-well border border-trs12-edge px-3.5 font-trs12-mono text-sm text-trs12-ink placeholder:text-trs12-mut outline-none transition-colors duration-200 focus-visible:border-(--tone) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone)">
</label>
<div class="flex items-center justify-between gap-3 mb-3">
<span class="text-[13px] text-trs12-mut">Strength level</span>
<span class="rounded-full px-2.5 py-1 text-xs font-bold transition-[background-color,color] duration-400 motion-reduce:transition-none" id="trs-12-badge" aria-live="polite">Fair</span>
</div>
<div class="flex gap-1 mb-6" id="trs-12-segs" aria-hidden="true">
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
<span class="flex-1 h-1.5 rounded-full bg-trs12-edge data-[on=true]:bg-(--tone)" data-on="false"></span>
</div>
<div class="relative h-12 flex items-center mb-5">
<div class="absolute inset-x-0 h-2.5 rounded-full bg-trs12-edge"></div>
<div class="absolute left-0 h-2.5 rounded-full w-[calc(var(--v)*100%)] bg-(--tone)"></div>
<input type="range" min="4" max="32" value="8" id="trs-12-inp" aria-label="Password length"
class="absolute inset-0 w-full h-12 appearance-none bg-transparent outline-none cursor-pointer z-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--tone) rounded-full">
</div>
<div class="flex items-baseline gap-2 mb-6">
<span class="text-[64px] leading-none font-bold tracking-tighter tabular-nums text-(--tone)" id="trs-12-num">8</span>
<span class="text-[13px] text-trs12-mut">characters</span>
</div>
<ul class="flex flex-col gap-2 m-0 p-0 list-none" id="trs-12-rules"></ul>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-trs12-bg: #0f172a;
--color-trs12-bg: oklch(0.21 0.03 262);
--color-trs12-card: #1c2740;
--color-trs12-card: oklch(0.27 0.035 262);
--color-trs12-well: #16203a;
--color-trs12-well: oklch(0.24 0.035 262);
--color-trs12-edge: #35435e;
--color-trs12-edge: oklch(0.38 0.035 262);
--color-trs12-ink: #f1f5f9;
--color-trs12-ink: oklch(0.97 0.006 255);
--color-trs12-mut: #8494ad;
--color-trs12-mut: oklch(0.66 0.03 255);
--color-trs12-weak: #ef4444;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: #f59e0b;
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: #10b981;
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: #06b6d4;
--color-trs12-strong: oklch(0.73 0.13 205);
--font-trs12-sans: 'Space Grotesk', system-ui, sans-serif;
--font-trs12-mono: 'JetBrains Mono', ui-monospace, monospace;
}
@property --v {
syntax: "<number>";
inherits: true;
initial-value: 0.143;
}
/* Zone colours are read by raw CSS (the thumb) and by JS, never by a generated
utility, so v4 would tree-shake them out of @theme — restated here. */
/* --tone is registered as a <color> and animated here; its consumers must NOT
carry their own paint transitions or they freeze at the first-paint zone. */
@property --tone {
syntax: "<color>";
inherits: true;
initial-value: oklch(0.76 0.16 70);
}
.trs-12 {
transition: --tone .4s ease;
--color-trs12-weak: oklch(0.63 0.22 25);
--color-trs12-fair: oklch(0.76 0.16 70);
--color-trs12-good: oklch(0.7 0.16 165);
--color-trs12-strong: oklch(0.73 0.13 205);
width: 100%;
min-height: 100svh;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
box-shadow: 0 2px 12px oklch(0 0 0 / 0.45);
cursor: grab;
transition: border-color .4s, transform .15s;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: scale(1.14);
cursor: grabbing;
}
.trs-12 input[type="range"]::-moz-range-thumb {
width: 26px;
height: 26px;
border-radius: 9999px;
box-sizing: border-box;
background: #fff;
border: 3px solid var(--tone);
cursor: grab;
}
@media (prefers-reduced-motion: reduce) {
.trs-12 {
transition: none;
}
.trs-12 input[type="range"]::-webkit-slider-thumb {
transition: none;
}
.trs-12 input[type="range"]:active::-webkit-slider-thumb {
transform: none;
}
}
```
## JavaScript
```js
(function(){
var root = document.getElementById('trs-12-root');
var inp = document.getElementById('trs-12-inp');
var pw = document.getElementById('trs-12-pw');
var num = document.getElementById('trs-12-num');
var badge = document.getElementById('trs-12-badge');
var shield = document.getElementById('trs-12-shield');
var segs = Array.prototype.slice.call(document.getElementById('trs-12-segs').children);
var rulesEl = document.getElementById('trs-12-rules');
var LEVELS = [
{ max: 7, tone: 'var(--color-trs12-weak)', label: 'Weak', badgeBg: 'oklch(0.95 0.03 25)', badgeInk: 'oklch(0.4 0.16 25)', icon: '🔓', segs: 1 },
{ max: 11, tone: 'var(--color-trs12-fair)', label: 'Fair', badgeBg: 'oklch(0.96 0.04 85)', badgeInk: 'oklch(0.45 0.13 70)', icon: '🔒', segs: 2 },
{ max: 19, tone: 'var(--color-trs12-good)', label: 'Good', badgeBg: 'oklch(0.95 0.04 165)', badgeInk: 'oklch(0.42 0.12 165)',icon: '🛡️', segs: 3 },
{ max: 99, tone: 'var(--color-trs12-strong)', label: 'Strong', badgeBg: 'oklch(0.95 0.04 205)', badgeInk: 'oklch(0.42 0.1 205)', icon: '🔐', segs: 4 }
];
var RULES = [[8, '8+ characters for basic security'], [12, '12+ characters recommended'], [20, '20+ characters for maximum security']];
var lastLabel = '';
RULES.forEach(function(r){
var li = document.createElement('li');
li.dataset.ok = 'false';
li.className = 'flex items-center gap-2.5 text-[13px] text-trs12-mut transition-colors duration-300 data-[ok=true]:text-trs12-ink motion-reduce:transition-none';
li.innerHTML = '<span class="grid place-items-center size-5 shrink-0 rounded-full bg-trs12-edge text-[11px] transition-colors duration-300 data-[ok=true]:bg-trs12-good motion-reduce:transition-none" data-ok="false" aria-hidden="true">✗</span><span>' + r[1] + '</span>';
rulesEl.appendChild(li);
});
var rows = Array.prototype.slice.call(rulesEl.children);
function level(v){ for (var i = 0; i < LEVELS.length; i++) if (v <= LEVELS[i].max) return LEVELS[i]; return LEVELS[3]; }
function update(){
var v = +inp.value, lv = level(v);
root.style.setProperty('--tone', lv.tone);
root.style.setProperty('--v', (v - +inp.min) / (+inp.max - +inp.min));
num.textContent = v;
shield.textContent = lv.icon;
if (lv.label !== lastLabel) {
badge.textContent = lv.label;
badge.style.backgroundColor = lv.badgeBg;
badge.style.color = lv.badgeInk;
lastLabel = lv.label;
}
segs.forEach(function(s, i){ s.dataset.on = i < lv.segs ? 'true' : 'false'; });
rows.forEach(function(row, i){
var ok = v >= RULES[i][0];
row.dataset.ok = ok ? 'true' : 'false';
var icon = row.firstElementChild;
icon.dataset.ok = ok ? 'true' : 'false';
icon.textContent = ok ? '✓' : '✗';
});
inp.setAttribute('aria-valuetext', lv.label + ', ' + v + ' characters');
}
inp.addEventListener('input', function(){ update(); });
pw.addEventListener('input', function(){
inp.value = Math.min(+inp.max, Math.max(+inp.min, pw.value.length));
update();
});
update();
})();
```How this works
One property, many consumers. getLevel(v) resolves a zone and writes --tone; the shield tint, badge, fill, numeral, thumb border and active segments all read it via [var(--tone)] utilities. The original assigned .style.background, .style.color and .style.boxShadow across six nodes.
The badge's two-colour pair is data, not a hack. The CSS version encoded it as '#fef2f2/#991b1b' and split the string; here each zone carries badgeBg and badgeInk as separate fields, so nothing has to be parsed.
The password field is the upgrade. A strength meter detached from a field is a toy; wiring the real input's length to the slider (and keeping the slider for exploration) makes it a component you could ship.
Make it yours
- Zones: add a fifth tier to
LEVELSand a fifth segment — the segment loop reads the array. - Scoring: swap length for entropy (
length × charset diversity) and map it onto the same 0-32 range. - Checklist: the
RULESarray (threshold + label) renders the rows.
Gotchas — read before shipping
- An inline
style.backgroundbeats any class. Driving everything through--toneavoids that fight entirely — nothing here sets a colour property directly. - Do not announce on every keystroke. The badge is
aria-live="polite"and the controller only rewrites it when the ZONE changes. - Emoji shields render at different optical sizes per platform; the container fixes the box so the layout cannot shift.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Custom-property colour consumption via bg-[var(--tone)] works everywhere Tailwind v4 does; no vendor prefixes beyond the thumb block.