Baktiyar Ablaikhan

Dropdown

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

Open the menu and glide over the items; the highlight follows you.

Code

"use client";

import { AnimatePresence, motion } from "motion/react";
import { Archive, Check, ChevronDown, Copy, Pencil, Share2, Trash2 } from "lucide-react";
import { useId, useState } from "react";
import { Surface } from "../ui";

const ITEMS = [
  { label: "Rename", Icon: Pencil, key: "R" },
  { label: "Duplicate", Icon: Copy, key: "D" },
  { label: "Share", Icon: Share2, key: "S" },
  { label: "Archive", Icon: Archive, key: "A" },
  { label: "Delete", Icon: Trash2, key: "⌫", danger: true },
];

export default function Dropdown() {
  const [open, setOpen] = useState(false);
  const [hot, setHot] = useState<number | null>(null);
  const [done, setDone] = useState<string | null>(null);
  const id = useId();

  return (
    <Surface className="place-items-start justify-items-center pt-[7%]">
      <div className="relative">
        <motion.button
          type="button"
          whileTap={{ scale: 0.96 }}
          onClick={() => setOpen((o) => !o)}
          className="flex items-center gap-[0.6em] rounded-[0.8em] bg-white px-[1em] py-[0.6em] text-[0.9em] font-medium shadow-[0_6px_20px_-12px_rgba(0,0,0,0.3),0_0_0_1px_rgba(0,0,0,0.05)]"
        >
          <AnimatePresence mode="popLayout" initial={false}>
            <motion.span key={done ?? "o"} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} className="flex items-center gap-[0.4em]">
              {done ? (
                <>
                  <Check className="h-[1em] w-[1em] text-[#2B46B8]" /> {done}
                </>
              ) : (
                "Options"
              )}
            </motion.span>
          </AnimatePresence>
          <motion.span animate={{ rotate: open ? 180 : 0 }}>
            <ChevronDown className="h-[1em] w-[1em] text-[#8a8984]" />
          </motion.span>
        </motion.button>
        <AnimatePresence>
          {open && (
            <motion.div
              initial={{ opacity: 0, scale: 0.92, y: -6, filter: "blur(4px)" }}
              animate={{ opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }}
              exit={{ opacity: 0, scale: 0.95, y: -4, filter: "blur(4px)" }}
              transition={{ type: "spring", stiffness: 500, damping: 34 }}
              style={{ transformOrigin: "50% 0%" }}
              className="absolute left-1/2 top-full z-10 mt-[0.5em] w-[13em] -translate-x-1/2 rounded-[0.9em] bg-white p-[0.35em] shadow-[0_16px_40px_-18px_rgba(0,0,0,0.4),0_0_0_1px_rgba(0,0,0,0.05)]"
              onPointerLeave={() => setHot(null)}
            >
              {ITEMS.map((it, i) => (
                <motion.button
                  key={it.label}
                  type="button"
                  initial={{ opacity: 0, x: -6 }}
                  animate={{ opacity: 1, x: 0, transition: { delay: 0.03 * i } }}
                  onPointerEnter={() => setHot(i)}
                  onClick={() => {
                    setDone(it.label);
                    setOpen(false);
                  }}
                  className={`relative flex w-full items-center gap-[0.6em] rounded-[0.6em] px-[0.7em] py-[0.42em] text-left text-[0.85em] ${it.danger ? "text-[#d93b3b]" : "text-[#151515]"} ${i === ITEMS.length - 1 ? "mt-[0.3em]" : ""}`}
                >
                  {hot === i && (
                    <motion.span
                      layoutId={`${id}-hl`}
                      className={`absolute inset-0 rounded-[0.6em] ${it.danger ? "bg-[#fdecec]" : "bg-[#f1f0ec]"}`}
                      transition={{ type: "spring", stiffness: 600, damping: 40 }}
                    />
                  )}
                  <it.Icon className="relative h-[1.05em] w-[1.05em]" />
                  <span className="relative flex-1">{it.label}</span>
                  <span className="relative font-mono text-[0.8em] text-[#a9a8a2]">{it.key}</span>
                </motion.button>
              ))}
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </Surface>
  );
}
Prompt

Build "Dropdown", a navigation animation. What it does: Open the menu and glide over the items; the highlight follows you. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using lucide-react, 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).