Switch-to-Slider Morph
A React switch and slider that share one thumb: turning it on makes the thumb leap onto a vertical or horizontal slider to set a value, turning it off sends it back. Built for enable-then-adjust settings.
A setting that is first switched on and then tuned needs only one thumb. While off, the thumb sits in the switch; press it and the slider track unfolds while the thumb springs across, growing slightly mid-flight as if it leapt, lands on min and then counts up to the current value. Once on, drag the thumb or click the track to adjust, and a value bubble with rolling digits follows the thumb while you drag; press the switch again and the thumb flies back before the track folds away. The on/off state and the value each support controlled and uncontrolled modes, and the slider can be vertical or horizontal.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add switch-to-slider-morph.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SwitchToSliderMorph/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/switch-to-slider-morph.en.jsonOr, once registries are configured in components.json, install it as @webberui/switch-to-slider-morph.
Usage
import { SwitchToSliderMorph } from "@/components/ui/switch-to-slider-morph";
const [on, setOn] = React.useState(false);
const [level, setLevel] = React.useState(72);
<SwitchToSliderMorph
label="Focus mode"
enabled={on}
onEnabledChange={setOn}
value={level}
onValueChange={setLevel}
valueLabel={(v) => `Noise ${v}%`}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
enabled | boolean | — | Controlled on/off state; when omitted the component manages it internally (uncontrolled) |
defaultEnabled | boolean | false | Initial on/off state in uncontrolled mode |
onEnabledChange | (enabled: boolean) => void | — | Callback when the on/off state changes |
value | number | — | Controlled slider value; when omitted the component manages it internally (uncontrolled) |
defaultValue | number | min | Initial value in uncontrolled mode |
onValueChange | (value: number) => void | — | Callback when the value changes (already snapped to step and clamped to min–max) |
min | number | 0 | Minimum value |
max | number | 100 | Maximum value |
step | number | 1 | Value increment |
orientation | "vertical" | "horizontal" | "vertical" | Slider orientation |
length | number | 160 | Slider track length (px) |
label | string | "啟用" (Enable) | Label text, also the accessible name of both the switch and the slider |
valueLabel | (value: number) => string | (v) => `${v}%` | Value formatter, also used for aria-valuetext |
countUp | boolean | true | On enable, the thumb lands on min first, then counts up to the current value |
stiffness | number | 500 | Stiffness of the thumb's flight spring |
damping | number | 28 | Damping of the thumb's flight spring |
disabled | boolean | false | Disable the switch and the slider |
className | string | — | Passed through to the outermost container |
How it works
- Shared thumb: the thumb in the switch and the thumb on the slider are the same motion
layoutId(inside aLayoutGroup), so toggling springs it from the old place to the new one. Layout animation travels in a straight line, so an extra 1 → 1.2 → 1 scale mid-flight makes it read as a leap. - The slider track unfolds with
clip-pathrather than a real height change — the thumb's landing spot is measured during render, and a zero-size track would send it to the wrong place. When turning off, the track waits about 200ms before folding so the thumb flies home first. - Dragging moves the thumb with a transform offset, which does not trigger layout animation, so it tracks the pointer without lag;
setPointerCapturekeeps tracking even outside the track, and clicking anywhere on the track jumps to that value. - The count-up only happens at the moment of enabling and only when the value is above min; dragging or a key press stops it immediately.
- CSS variables:
--wb-knob-slider-accent(default#3b82f6, switch and fill colour),--wb-knob-slider-expand(default220ms, track unfold duration),--wb-knob-slider-count(default600ms, count-up duration).
Accessibility
- The switch is a
role="switch"button witharia-checked; the slider thumb isrole="slider"witharia-valuenow/aria-valuemin/aria-valuemax/aria-orientation, andaria-valuetextuses the output ofvalueLabel - Both point at
labelthrougharia-labelledby; while off, the slider track is hidden from assistive technology - Keyboard: arrow keys ±
step, PageUp / PageDown ±10 steps, Home / End jump to min / max; the switch and the thumb both show a focus-visible ring - Turning it on with the switch moves focus to the slider thumb, turning it off returns focus to the switch; controlled changes from outside never steal focus
- The value bubble and the readout next to the label are visual only (
aria-hidden); semantics come fromaria-valuetext - When the user's system has reduce motion turned on, the thumb does not leap or count — it fades in at its new place over 120ms, and the track fades instead of unfolding
Dial Knob
A rotary knob input: an SVG tick ring, a subtle nudge at detents, a spring-settled pointer and a rolling readout, with wheel and arrow-key support.
Magnetic Button
A magnetic button that is pulled towards the cursor as it approaches, with a parallax on the inner text and a spring back once the cursor leaves.