Skip to content

Share Button Expand

A share icon that expands into a row of social options with a copy-link state.

Freeminimalplayful
Category
Components
Added
2026-08-24
Deps
2
Updated
2026-09-01
Preview

Props

This entry takes no configurable props.

Requires framer-motion, lucide-react
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

NewPro

A 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.

buttonhovertext-animation
Featured

Magnetic Button

components

A button whose label leans toward the cursor with a springy pull.

buttoncursorspring
Trending

A button that scatters a quick burst of confetti particles on click.

buttonconfetticlick