Forms
Precision Slider
A calibrated native range control with illuminated ticks, a machined thumb, and a live numeric readout.
Original by Plinth · MIT
Report a problem or suggest an improvementInstall
npx shadcn@latest add https://plinthui.com/r/precision-slider.json"use client";
import { useState, type CSSProperties } from "react";
const css = String.raw`
.ps-control {
display: block;
--ps-ink: #26322e;
--ps-muted: #68716c;
--ps-surface: #f3f4ef;
--ps-line: #dce0d7;
--ps-accent: #bf4c22;
width: min(320px, 100%);
box-sizing: border-box;
padding: 22px 24px 18px;
border: 1px solid var(--ps-line);
border-radius: 22px;
color: var(--ps-ink);
background: linear-gradient(160deg, #fff8, var(--ps-surface) 55%);
box-shadow:
0 16px 35px -24px #0005,
inset 0 1px #fff8;
font:
14px/1.4 system-ui,
sans-serif;
}
.ps-control * {
box-sizing: border-box;
}
.ps-control[data-theme="dark"],
:root[data-theme="dark"] .ps-control {
--ps-ink: #eeeee7;
--ps-muted: #a7aea4;
--ps-surface: #252925;
--ps-line: #3b423a;
--ps-accent: #ff956b;
background: linear-gradient(160deg, #343a32, var(--ps-surface) 55%);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .ps-control {
--ps-ink: #eeeee7;
--ps-muted: #a7aea4;
--ps-surface: #252925;
--ps-line: #3b423a;
--ps-accent: #ff956b;
background: linear-gradient(160deg, #343a32, var(--ps-surface) 55%);
}
}
.ps-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.ps-label {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
}
.ps-value {
font-size: 32px;
font-weight: 500;
letter-spacing: -0.06em;
font-variant-numeric: tabular-nums;
}
.ps-value small {
margin-left: 3px;
font-size: 14px;
color: var(--ps-muted);
letter-spacing: 0;
}
.ps-meter {
position: relative;
margin-top: 22px;
padding: 0 9px;
display: flex;
justify-content: space-between;
align-items: end;
height: 35px;
}
.ps-tick {
width: 3px;
height: 16px;
border-radius: 2px;
background: var(--ps-line);
transition:
background 120ms,
height 160ms;
}
.ps-tick:nth-child(5n + 1) {
height: 29px;
}
.ps-tick[data-active="true"] {
background: var(--ps-accent);
box-shadow: 0 0 6px color-mix(in srgb, var(--ps-accent) 18%, transparent);
}
.ps-range {
display: block;
appearance: none;
-webkit-appearance: none;
width: 100%;
height: 44px;
margin: 0;
cursor: ew-resize;
background: transparent;
outline-offset: 3px;
}
.ps-range::-webkit-slider-runnable-track {
height: 4px;
border-radius: 4px;
background: linear-gradient(
to right,
var(--ps-accent) var(--ps-value, 62%),
var(--ps-line) var(--ps-value, 62%)
);
}
.ps-range::-moz-range-track {
height: 4px;
border-radius: 4px;
background: var(--ps-line);
}
.ps-range::-moz-range-progress {
height: 4px;
background: var(--ps-accent);
}
.ps-range::-webkit-slider-thumb {
appearance: none;
-webkit-appearance: none;
width: 19px;
height: 27px;
margin-top: -12px;
border-radius: 7px;
border: 1px solid #8888;
background: linear-gradient(
90deg,
#fafbf6 0%,
#c9cec3 42%,
#fbfcf9 48%,
#aab3a3 53%,
#f3f5ec 60%,
#c1c8b7
);
box-shadow:
0 2px 4px #0003,
0 0 0 3px var(--ps-surface);
}
.ps-range::-moz-range-thumb {
width: 19px;
height: 27px;
border-radius: 7px;
border: 1px solid #8888;
background: linear-gradient(90deg, #fafbf6, #b8c0b0, #f3f5ec);
box-shadow: 0 2px 4px #0003;
}
.ps-range:focus-visible {
outline: 2px solid var(--ps-accent);
border-radius: 5px;
}
.ps-scale {
display: flex;
justify-content: space-between;
color: var(--ps-muted);
font:
10px/1.4 ui-monospace,
monospace;
letter-spacing: 0.06em;
}
@media (prefers-reduced-motion: reduce) {
.ps-tick {
transition: none;
}
}
`;
/** A native range input keeps touch, keyboard and assistive input intact. */
export function PrecisionSlider({
label = "Intensity",
defaultValue = 62,
onValueChange,
}: {
label?: string;
defaultValue?: number;
onValueChange?: (value: number) => void;
}) {
const [value, setValue] = useState(() =>
Number.isFinite(defaultValue)
? Math.min(100, Math.max(0, Math.round(defaultValue)))
: 62,
);
return (
<>
<style>{css}</style>
<label
className="ps-control"
style={{ "--ps-value": `${value}%` } as CSSProperties}
>
<span className="ps-top">
<span className="ps-label">{label}</span>
<span className="ps-value" aria-hidden="true">
<span data-value>{value}</span>
<small>%</small>
</span>
</span>
<span className="ps-meter" aria-hidden="true">
{Array.from({ length: 31 }, (_, i) => (
<span
className="ps-tick"
key={i}
data-active={i <= (value * 30) / 100}
/>
))}
</span>
<input
className="ps-range"
aria-label={label}
aria-valuetext={`${value} percent`}
type="range"
min="0"
max="100"
step="1"
value={value}
onChange={(event) => {
const next = Number(event.target.value);
setValue(next);
onValueChange?.(next);
}}
/>
<span className="ps-scale" aria-hidden="true">
<span>0 / LOW</span>
<span>100 / HIGH</span>
</span>
</label>
</>
);
}