Resizable panels
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag the divider to resize the panels; it snaps shut near the edges.
Code
"use client";
import { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from "motion/react";
import { useRef, useState } from "react";
import { Surface } from "../ui";
export default function ResizablePanels() {
const box = useRef<HTMLDivElement>(null);
const split = useMotionValue(0.42);
const [drag, setDrag] = useState(false);
const [pct, setPct] = useState(42);
const left = useTransform(split, (v) => `${v * 100}%`);
useMotionValueEvent(split, "change", (v) => setPct(Math.round(v * 100)));
const at = (x: number) => {
const r = box.current!.getBoundingClientRect();
return (x - r.left) / r.width;
};
return (
<Surface className="p-[1.4em]">
<div ref={box} className="relative flex h-full w-full overflow-hidden rounded-[1em] bg-white shadow-[0_12px_32px_-18px_rgba(0,0,0,0.35),0_0_0_1px_rgba(0,0,0,0.04)]">
<motion.div style={{ width: left }} className="relative overflow-hidden p-[1em]">
<p className="font-mono text-[0.65em] uppercase tracking-[0.15em] text-[#8a8984]">Files</p>
{["index.tsx", "panel.tsx", "styles.css", "README.md"].map((f, i) => (
<div key={f} className={`mt-[0.5em] truncate rounded-[0.4em] px-[0.5em] py-[0.25em] text-[0.8em] ${i === 1 ? "bg-[#f1f0ec]" : ""}`}>
{f}
</div>
))}
</motion.div>
<div
className="group relative z-10 w-[1.2em] shrink-0 cursor-col-resize touch-none"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
setDrag(true);
}}
onPointerMove={(e) => {
if (!drag) return;
const t = at(e.clientX);
// resist near the edges, then snap shut
split.set(Math.min(0.85, Math.max(0.06, t)));
}}
onPointerUp={() => {
setDrag(false);
const v = split.get();
if (v < 0.16) animate(split, 0.06, { type: "spring", stiffness: 400, damping: 30 });
else if (v > 0.78) animate(split, 0.85, { type: "spring", stiffness: 400, damping: 30 });
}}
onDoubleClick={() => animate(split, 0.42, { type: "spring", stiffness: 300, damping: 26 })}
>
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-[#ecebe6]" />
<motion.div
animate={{ height: drag ? "3.6em" : "2em", backgroundColor: drag ? "#2B46B8" : "#cfcec8" }}
className="absolute left-1/2 top-1/2 w-[0.3em] -translate-x-1/2 -translate-y-1/2 rounded-full group-hover:bg-[#151515]"
/>
<motion.span
animate={{ opacity: drag ? 1 : 0, y: drag ? 0 : 4 }}
className="pointer-events-none absolute left-1/2 top-[calc(50%+2.4em)] -translate-x-1/2 rounded-full bg-[#151515] px-[0.5em] py-[0.1em] font-mono text-[0.6em] text-white"
>
{pct}%
</motion.span>
</div>
<div className="min-w-0 flex-1 bg-[#fbfaf7] p-[1em]">
<p className="font-mono text-[0.65em] uppercase tracking-[0.15em] text-[#8a8984]">panel.tsx</p>
<div className="mt-[0.7em] space-y-[0.5em]">
{[70, 88, 52, 94, 40, 76, 60].map((w, i) => (
<div key={i} className="flex items-center gap-[0.6em]">
<span className="w-[1.2em] text-right font-mono text-[0.6em] text-[#c9c8c3]">{i + 1}</span>
<div className="h-[0.45em] rounded-full" style={{ width: `${w}%`, background: i % 3 === 1 ? "#c9d1f3" : "#e4e3de" }} />
</div>
))}
</div>
</div>
</div>
</Surface>
);
}
Prompt
Build "Resizable panels", a layout animation. What it does: Drag the divider to resize the panels; it snaps shut near the edges. 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).