02 / Controls
Elastic Slider
A precise slider with a small amount of give at either end. The current value stays visible and the control works without a pointer.
"use client"
import * as React from "react"
import { Volume2 } from "lucide-react"
import { ElasticSlider } from "mischief-ui/elastic-slider"
export function ElasticSliderDemo() {
const [volume, setVolume] = React.useState(68)
return (
<div className="demo-content slider-demo">
<div className="preview-heading">
<span className="preview-icon">
<Volume2 aria-hidden="true" size={20} />
</span>
<div>
<strong>Notification volume</strong>
<p>Changes play immediately.</p>
</div>
</div>
<ElasticSlider
label="Volume"
value={volume}
onValueChange={setVolume}
className="[--background:oklch(0.215_0.018_58)] [--foreground:oklch(0.975_0.012_84)] [--muted:oklch(1_0_0/14%)] [--primary:oklch(0.78_0.16_128)]"
/>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/elastic-sliderimport { ElasticSlider } from "mischief-ui/elastic-slider"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Slider } from "@base-ui/react/slider"import { motion, useReducedMotion } from "motion/react"import { cn } from "@/lib/utils" export interface ElasticSliderProps extends Omit< React.ComponentPropsWithoutRef<typeof Slider.Root>, "children" | "defaultValue" | "onValueChange" | "onValueCommitted" | "value"> { label: React.ReactNode defaultValue?: number value?: numberUsage
export function Volume() {
return (
<ElasticSlider
label="Notification volume"
defaultValue={68}
name="volume"
/>
)
}While dragging, and after
There are two callbacks because there are two moments, and confusing them is expensive. onValueChange fires continuously through a drag, which is what you want for a preview that has to keep up. onValueCommitted fires once, when the handle is released or a key is lifted.
Anything with a cost belongs in the committed callback: a request, a write, an undo entry. Putting a save in onValueChange sends one for every frame of a single drag.
<ElasticSlider
label="Volume"
defaultValue={68}
onValueChange={setPreview}
onValueCommitted={(value) => save({ volume: value })}
/>Reading the value
The number shown beside the label comes from formatValue, and so does the value announced to a screen reader. Use it to give the number its unit, because a bare 68 says nothing about what it measures.
formatValue={(value) => `${value}%`}
formatValue={(value) => `${(value / 100).toFixed(2)} s`}min, max, and step are passed to the underlying Base UI slider, so a step of 5 constrains the keyboard as well as the drag. Motion is used only for the stretch: with reduced motion the handle still tracks exactly, it simply stops deforming.
API
labelReactNodeThe visible and accessible label.defaultValuenumberThe initial uncontrolled value. Defaults to 50.valuenumberThe current value when controlled.onValueChange(value: number) => voidRuns while the value changes.onValueCommitted(value: number) => voidRuns when interaction finishes.min, max, stepnumberRange and increment settings.formatValue(value: number) => stringFormats the visible value.Accessibility
The control uses Base UI slider behavior and a native output for the visible value. It supports pointer, touch, and keyboard input. End feedback is removed when reduced motion is enabled.