Skip to content
Plinth

Forms

Precision Slider

A calibrated native range control with illuminated ticks, a machined thumb, and a live numeric readout.

#range#slider#tactile#input

Install

npx shadcn@latest add https://plinthui.com/r/precision-slider.json
Open in v0
"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>
    </>
  );
}

More in Forms