Elastischer Lautstärkeregler
Ein Lautstärkeregler mit Gummi: Ziehst du übers Ende hinaus, dehnt sich der Balken mit Widerstand und schnellt beim Loslassen zurück.
Einstellungen
Code
import { useRef, useState } from "react";
import type { KeyboardEvent, PointerEvent } from "react";
import { animate, motion, useMotionValue, useReducedMotion, useTransform } from "framer-motion";
import { Volume, Volume2, VolumeX } from "lucide-react";
interface ElasticSliderProps {
label?: string;
color?: string;
stretch?: number;
}
export default function ElasticSlider({
label = "Lautstärke",
color = "#5e5ce6",
stretch = 28,
}: ElasticSliderProps) {
const [value, setValue] = useState(60);
const [active, setActive] = useState(false);
const [hover, setHover] = useState(false);
const track = useRef<HTMLDivElement>(null);
const dragging = useRef(false);
const last = useRef(60);
const overflow = useMotionValue(0);
const lowPulse = useMotionValue(1);
const highPulse = useMotionValue(1);
const reduce = useReducedMotion();
const scaleX = useTransform(overflow, (o) => 1 + Math.abs(o) / (track.current?.offsetWidth || 200));
const scaleY = useTransform(overflow, (o) => 1 - Math.min(Math.abs(o) / 90, 0.3));
const originX = useTransform(overflow, (o) => (o < 0 ? 1 : 0));
const lowX = useTransform(overflow, (o) => (o < 0 ? o : 0));
const highX = useTransform(overflow, (o) => (o > 0 ? o : 0));
const commit = (next: number) => {
const v = Math.min(100, Math.max(0, Math.round(next)));
if (!reduce && v !== last.current) {
if (v === 0) animate(lowPulse, [1, 1.3, 1], { duration: 0.35 });
if (v === 100) animate(highPulse, [1, 1.3, 1], { duration: 0.35 });
}
last.current = v;
setValue(v);
};
const follow = (clientX: number) => {
const el = track.current;
if (!el) return;
const r = el.getBoundingClientRect();
commit(((clientX - r.left) / r.width) * 100);
const raw = clientX < r.left ? clientX - r.left : clientX > r.right ? clientX - r.right : 0;
const max = Math.max(0, stretch);
overflow.set(reduce || max === 0 ? 0 : Math.sign(raw) * max * (1 - Math.exp(-Math.abs(raw) / (max * 2.5))));
};
const release = () => {
dragging.current = false;
setActive(false);
animate(overflow, 0, { type: "spring", stiffness: 420, damping: 18 });
};
const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
e.currentTarget.setPointerCapture(e.pointerId);
dragging.current = true;
setActive(true);
follow(e.clientX);
};
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
const step = e.shiftKey ? 10 : 5;
const map: Record<string, number> = {
ArrowRight: value + step,
ArrowUp: value + step,
ArrowLeft: value - step,
ArrowDown: value - step,
Home: 0,
End: 100,
};
const target = map[e.key];
if (target === undefined) return;
e.preventDefault();
commit(target);
};
const big = active || hover;
return (
<div className="flex w-[17rem] max-w-full select-none items-center gap-3 text-[#86868b]">
<motion.span style={{ x: lowX, scale: lowPulse }} className="grid h-5 w-5 shrink-0 place-items-center">
{value === 0 ? <VolumeX className="h-[18px] w-[18px]" strokeWidth={1.75} /> : <Volume className="h-[18px] w-[18px]" strokeWidth={1.75} />}
</motion.span>
<div
role="slider"
tabIndex={0}
aria-label={label}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={value}
onPointerDown={onPointerDown}
onPointerMove={(e) => dragging.current && follow(e.clientX)}
onPointerUp={release}
onPointerCancel={release}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
onKeyDown={onKeyDown}
className="relative flex h-10 flex-1 cursor-grab touch-none items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-black/20 active:cursor-grabbing dark:focus-visible:ring-white/30"
>
<motion.div
ref={track}
className="relative w-full overflow-hidden rounded-full bg-black/[0.08] dark:bg-white/[0.14]"
style={{ scaleX, scaleY, originX }}
initial={false}
animate={{ height: big ? 12 : 6 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<motion.div
className="absolute inset-y-0 left-0"
style={{ backgroundColor: color }}
initial={false}
animate={{ width: value + "%" }}
transition={active ? { duration: 0 } : { type: "spring", stiffness: 400, damping: 36 }}
/>
</motion.div>
</div>
<motion.span style={{ x: highX, scale: highPulse }} className="grid h-5 w-5 shrink-0 place-items-center">
<Volume2 className="h-[18px] w-[18px]" strokeWidth={1.75} />
</motion.span>
</div>
);
}
Prompt für KI
Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.
Mehr aus Mikrointeraktionen
Wofür du ihn nutzen kannst
Dieser elastische Slider übernimmt das Gefühl der Lautstärkeregelung aktueller Smartphones. Er passt in einen Musik- oder Podcast-Player, in die Einstellungen einer Video-App oder als Helligkeitsregler in einem Smart-Home-Dashboard. Der Balken wird beim Hovern dicker, die Lautsprecher-Icons pulsieren bei null und bei voll, und über den Rand hinaus dehnt er sich wie ein Gummiband.
Fang mit einer Elastizität von 20 bis 30 Pixeln an, bei null bekommst du einen ganz normalen Slider. Das Dehnen passiert nur beim Ziehen. Per Tastatur bewegen ihn die Pfeiltasten in Fünferschritten, mit gedrückter Umschalttaste in Zehnerschritten.