Budget slider
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag the slider to set a budget and watch every category follow.
Code
"use client";
import { animate, motion, useMotionValue, useMotionValueEvent, useSpring, useTransform } from "motion/react";
import { useRef, useState } from "react";
import { Surface } from "../ui";
const MIN = 500;
const MAX = 6000;
const SPLIT = [
{ name: "Rent", share: 0.46, color: "#151515" },
{ name: "Food", share: 0.22, color: "#2B46B8" },
{ name: "Travel", share: 0.18, color: "#8ea0e6" },
{ name: "Other", share: 0.14, color: "#c9c8c3" },
];
const fmt = (n: number) => `$${Math.round(n / 10) * 10}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
export default function BudgetSlider() {
const track = useRef<HTMLDivElement>(null);
const t = useMotionValue(0.35);
const smooth = useSpring(t, { stiffness: 400, damping: 40 });
const left = useTransform(smooth, (v) => `${v * 100}%`);
const [amount, setAmount] = useState(MIN + 0.35 * (MAX - MIN));
const [press, setPress] = useState(false);
useMotionValueEvent(smooth, "change", (v) => setAmount(MIN + v * (MAX - MIN)));
const at = (x: number) => {
const r = track.current!.getBoundingClientRect();
return Math.min(1, Math.max(0, (x - r.left) / r.width));
};
return (
<Surface>
<div className="w-[20em] rounded-[1.3em] bg-white p-[1.3em] shadow-[0_12px_32px_-18px_rgba(0,0,0,0.35)]">
<p className="text-[0.75em] text-[#8a8984]">Monthly budget</p>
<p className="mt-[0.1em] text-[2.2em] font-medium tabular-nums tracking-[-0.02em]">{fmt(amount)}</p>
<div
ref={track}
className="relative mt-[1em] h-[2.4em] cursor-pointer touch-none"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
setPress(true);
t.set(at(e.clientX));
}}
onPointerMove={(e) => press && t.set(at(e.clientX))}
onPointerUp={() => {
setPress(false);
// snap to a round hundred
const snapped = (Math.round((MIN + t.get() * (MAX - MIN)) / 100) * 100 - MIN) / (MAX - MIN);
animate(t, snapped, { duration: 0.2 });
}}
onPointerCancel={() => setPress(false)}
>
<div className="absolute inset-x-0 top-1/2 h-[0.5em] -translate-y-1/2 rounded-full bg-[#ecebe6]" />
<motion.div className="absolute left-0 top-1/2 h-[0.5em] -translate-y-1/2 rounded-full bg-[#151515]" style={{ width: left }} />
<motion.div className="absolute top-1/2" style={{ left }}>
<motion.div
animate={{ scale: press ? 1.25 : 1 }}
className="-ml-[0.8em] -mt-[0.8em] h-[1.6em] w-[1.6em] rounded-full border-[0.3em] border-[#151515] bg-white shadow-[0_4px_10px_-4px_rgba(0,0,0,0.5)]"
/>
<motion.span
animate={{ opacity: press ? 1 : 0, y: press ? 0 : 6 }}
className="pointer-events-none absolute bottom-[1.3em] left-0 -translate-x-1/2 whitespace-nowrap rounded-full bg-[#151515] px-[0.6em] py-[0.15em] text-[0.7em] text-white"
>
{fmt(amount)}
</motion.span>
</motion.div>
</div>
<div className="mt-[0.8em] flex h-[0.6em] gap-[0.2em] overflow-hidden rounded-full">
{SPLIT.map((s) => (
<div key={s.name} style={{ flex: s.share, background: s.color }} />
))}
</div>
<div className="mt-[0.8em] grid grid-cols-2 gap-x-[1em] gap-y-[0.3em] text-[0.75em]">
{SPLIT.map((s) => (
<div key={s.name} className="flex items-center justify-between">
<span className="flex items-center gap-[0.4em] text-[#8a8984]">
<span className="h-[0.6em] w-[0.6em] rounded-full" style={{ background: s.color }} />
{s.name}
</span>
<span className="tabular-nums">{fmt(amount * s.share)}</span>
</div>
))}
</div>
</div>
</Surface>
);
}
Prompt
Build "Budget slider", a input animation. What it does: Drag the slider to set a budget and watch every category follow. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).