Mischief

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.

Notification volume

Changes play immediately.

Volume
68%

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/elastic-slider
import { ElasticSlider } from "mischief-ui/elastic-slider"
Also installs
  • @base-ui/react
  • motion

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/elastic-slider/elastic-slider.tsx
"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?: number

Usage

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.