Share Button Expand
A share icon that expands into a row of social options with a copy-link state.
- Category
- Components
- Added
- 2026-08-24
- Deps
- 2
- Updated
- 2026-09-01
Props
This entry takes no configurable props.
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Share2, X, Mail, Link2, Check } from "lucide-react";
const OPTIONS = [
{ key: "x", icon: X, label: "Share on X" },
{ key: "email", icon: Mail, label: "Share via email" },
{ key: "copy", icon: Link2, label: "Copy link" },
];
export function ShareButtonExpand({ url }: { url: string }) {
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
function handleOptionClick(key: string) {
if (key === "copy") {
navigator.clipboard?.writeText(url).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1400);
return;
}
setOpen(false);
}
return (
<motion.div
layout
transition={{ type: "spring", stiffness: 400, damping: 32 }}
className="inline-flex items-center gap-1 overflow-hidden rounded-full border border-neutral-200 p-1.5"
>
<button
aria-label={open ? "Close share options" : "Share"}
onClick={() => setOpen((v) => !v)}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-neutral-500 hover:text-red-500"
>
<Share2 size={15} />
</button>
<AnimatePresence initial={false}>
{open &&
OPTIONS.map(({ key, icon: Icon, label }, i) => (
<motion.button
key={key}
layout
aria-label={key === "copy" && copied ? "Link copied" : label}
initial={{ opacity: 0, scale: 0.6 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.6 }}
transition={{ duration: 0.15, delay: i * 0.04 }}
onClick={() => handleOptionClick(key)}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-neutral-500 hover:text-red-500"
>
{key === "copy" && copied ? <Check size={14} className="text-red-500" /> : <Icon size={14} />}
</motion.button>
))}
</AnimatePresence>
</motion.div>
);
}
About this component
An icon-only share button that just pops a menu open is fine, but the pill itself resizing to make room for the new options, rather than the options appearing over a fixed-width container, reads as more considered. That resize is handled by giving the whole wrapper Framer Motion's layout prop; because layout automatically interpolates a box's size whenever its measured content changes, the pill's width animates smoothly as option buttons mount and unmount as children, with no manual width tweening required. Each option staggers in with a small scale and opacity entrance timed by its index. The copy-link option is handled differently from the rest on purpose: instead of closing the menu like the other options do, it writes to the clipboard and swaps its icon for a checkmark for about 1.4 seconds before reverting, so the confirmation happens in place rather than through a separate toast. Reach for this over a plain dropdown menu when the option count is small and fixed; past three or four targets the pill has no scrolling or wrapping fallback.
Curator’s note
The pill's own width animates via Framer's `layout` prop on the wrapper rather than a hand-tweened width value — layout automatically interpolates the box size whenever its content (the mounted option buttons) changes.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Ripple Roll Button
componentsA label that rolls letter by letter to a second copy, rippling outward from wherever your pointer came in — and settling back toward where it left.
Magnetic Button
componentsA button whose label leans toward the cursor with a springy pull.
Confetti Burst Button
componentsA button that scatters a quick burst of confetti particles on click.