Share Menu
Original · freeshare menu with options
byWeb InnoventixReact + Tailwind
menusharemenus
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/menu-share.jsonmenu-share.tsx
"use client";
import {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
/* ------------------------------------------------------------------ */
/* Shareable resource (real, specific copy) */
/* ------------------------------------------------------------------ */
const SHARE_URL = "https://frontstack.dev/guides/menu-button-pattern";
const SHARE_TITLE = "The menu button pattern, done right";
const SHARE_TEXT =
"Roving tabindex, focus return, typeahead and the keyboard model that makes a dropdown feel native.";
/* ------------------------------------------------------------------ */
/* Icons (inline SVG only) */
/* ------------------------------------------------------------------ */
function ShareIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<circle cx="18" cy="5" r="2.6" />
<circle cx="6" cy="12" r="2.6" />
<circle cx="18" cy="19" r="2.6" />
<path d="m8.3 10.7 7.4-4.3M8.3 13.3l7.4 4.3" />
</svg>
);
}
function LinkIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<path d="M9 13a3.5 3.5 0 0 0 5 0l3-3a3.5 3.5 0 0 0-5-5l-1 1" />
<path d="M15 11a3.5 3.5 0 0 0-5 0l-3 3a3.5 3.5 0 0 0 5 5l1-1" />
</svg>
);
}
function CopyIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<rect x="9" y="9" width="11" height="11" rx="2.4" />
<path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" />
</svg>
);
}
function CheckIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.1}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<path d="m5 12.5 4.5 4.5L19 7" />
</svg>
);
}
function ChevronIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.9}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4"
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function DeviceShareIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<path d="M12 3v11" />
<path d="m8.5 6.5 3.5-3.5 3.5 3.5" />
<path d="M6 12H5a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-2-2h-1" />
</svg>
);
}
function XIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="h-[16px] w-[16px]"
>
<path d="M13.9 10.6 21.1 2h-2.06l-6.02 7.02L8.2 2H2l7.56 11-7.56 9h2.06l6.35-7.4L15.8 22H22l-8.1-11.4Zm-2.24 2.6-.74-1.05L4.9 3.6h2.9l4.72 6.75.74 1.05 6.14 8.78h-2.9l-4.9-7.03Z" />
</svg>
);
}
function LinkedInIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="h-[17px] w-[17px]"
>
<path d="M4.98 3.5A2.5 2.5 0 1 1 5 8.5a2.5 2.5 0 0 1-.02-5ZM3.2 9.25h3.56V21H3.2V9.25Zm5.86 0h3.41v1.6h.05c.48-.9 1.63-1.85 3.36-1.85 3.6 0 4.26 2.37 4.26 5.45V21h-3.55v-5.2c0-1.24-.02-2.84-1.73-2.84-1.74 0-2 1.35-2 2.75V21H9.06V9.25Z" />
</svg>
);
}
function FacebookIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className="h-[17px] w-[17px]"
>
<path d="M22 12.06C22 6.5 17.52 2 12 2S2 6.5 2 12.06c0 5 3.66 9.15 8.44 9.94v-7.03H7.9v-2.9h2.54V9.85c0-2.51 1.49-3.9 3.77-3.9 1.1 0 2.24.2 2.24.2v2.47h-1.26c-1.24 0-1.63.78-1.63 1.57v1.87h2.78l-.44 2.9h-2.34V22c4.78-.79 8.44-4.94 8.44-9.94Z" />
</svg>
);
}
function EmailIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-[18px] w-[18px]"
>
<rect x="3" y="5" width="18" height="14" rx="2.4" />
<path d="m4 7 8 6 8-6" />
</svg>
);
}
function ExternalIcon() {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.7}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-3.5 w-3.5"
>
<path d="M8 16 16 8" />
<path d="M9 8h7v7" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* Clipboard helper (with legacy fallback) */
/* ------------------------------------------------------------------ */
async function copyText(text: string): Promise<boolean> {
try {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch {
/* fall through to legacy path */
}
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
document.body.removeChild(ta);
return ok;
} catch {
return false;
}
}
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
type ShareItem = {
id: string;
label: string;
hint: string;
icon: ReactNode;
accent: string;
href?: string;
onSelect?: () => void;
closeOnSelect: boolean;
trailing?: ReactNode;
};
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export default function ShareMenu() {
const uid = useId();
const triggerId = `${uid}-trigger`;
const menuId = `${uid}-menu`;
const headingId = `${uid}-heading`;
const linkFieldId = `${uid}-linkfield`;
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const [copied, setCopied] = useState(false);
const [rowCopied, setRowCopied] = useState(false);
const [canNativeShare, setCanNativeShare] = useState(false);
const [announce, setAnnounce] = useState("");
const triggerRef = useRef<HTMLButtonElement | null>(null);
const popoverRef = useRef<HTMLDivElement | null>(null);
const itemRefs = useRef<Array<HTMLAnchorElement | HTMLButtonElement | null>>(
[],
);
const pendingFocus = useRef<"first" | "last">("first");
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const rowTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const prefersReduced = useReducedMotion();
const reduce = prefersReduced ?? false;
/* --- feature detection (client only) --- */
useEffect(() => {
if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
setCanNativeShare(true);
}
}, []);
useEffect(() => {
return () => {
if (copyTimer.current) clearTimeout(copyTimer.current);
if (rowTimer.current) clearTimeout(rowTimer.current);
};
}, []);
/* --- actions --- */
const copyLink = useCallback(async () => {
const ok = await copyText(SHARE_URL);
if (!ok) {
setAnnounce("Could not copy the link. Select it and copy manually.");
return;
}
setCopied(true);
setAnnounce("Link copied to clipboard.");
if (copyTimer.current) clearTimeout(copyTimer.current);
copyTimer.current = setTimeout(() => setCopied(false), 2200);
}, []);
const copyLinkRow = useCallback(async () => {
const ok = await copyText(SHARE_URL);
if (!ok) {
setAnnounce("Could not copy the link. Select it and copy manually.");
return;
}
setRowCopied(true);
setAnnounce("Link copied to clipboard.");
if (rowTimer.current) clearTimeout(rowTimer.current);
rowTimer.current = setTimeout(() => setRowCopied(false), 2200);
}, []);
const nativeShare = useCallback(async () => {
try {
if (typeof navigator !== "undefined" && typeof navigator.share === "function") {
await navigator.share({
title: SHARE_TITLE,
text: SHARE_TEXT,
url: SHARE_URL,
});
}
} catch {
/* user dismissed the share sheet */
}
}, []);
/* --- menu open/close --- */
const closeMenu = useCallback((returnFocus: boolean) => {
setOpen(false);
if (returnFocus) {
requestAnimationFrame(() => triggerRef.current?.focus());
}
}, []);
const openMenu = useCallback((focus: "first" | "last") => {
pendingFocus.current = focus;
setOpen(true);
}, []);
/* --- items --- */
const enc = encodeURIComponent;
const items = useMemo<ShareItem[]>(() => {
const list: ShareItem[] = [];
if (canNativeShare) {
list.push({
id: "native",
label: "Share via this device",
hint: "Open the system share sheet",
icon: <DeviceShareIcon />,
accent:
"text-violet-600 dark:text-violet-300 bg-violet-500/10 ring-violet-500/20",
onSelect: nativeShare,
closeOnSelect: true,
});
}
list.push({
id: "copy",
label: "Copy link",
hint: copied ? "Copied to clipboard" : "Grab a plain URL",
icon: copied ? <CheckIcon /> : <LinkIcon />,
accent: copied
? "text-emerald-600 dark:text-emerald-300 bg-emerald-500/10 ring-emerald-500/20"
: "text-sky-600 dark:text-sky-300 bg-sky-500/10 ring-sky-500/20",
onSelect: copyLink,
closeOnSelect: false,
trailing: copied ? (
<span className="sharemenu-badge inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-2 py-0.5 text-[11px] font-semibold text-emerald-700 dark:text-emerald-300">
Copied
</span>
) : undefined,
});
list.push({
id: "x",
label: "Post on X",
hint: "Share to your followers",
icon: <XIcon />,
accent:
"text-zinc-900 dark:text-zinc-100 bg-zinc-900/[0.06] dark:bg-white/10 ring-zinc-900/10 dark:ring-white/15",
href: `https://twitter.com/intent/tweet?url=${enc(SHARE_URL)}&text=${enc(SHARE_TEXT)}`,
closeOnSelect: true,
trailing: <ExternalIcon />,
});
list.push({
id: "linkedin",
label: "Share on LinkedIn",
hint: "Post to your network",
icon: <LinkedInIcon />,
accent:
"text-sky-700 dark:text-sky-300 bg-sky-500/10 ring-sky-500/20",
href: `https://www.linkedin.com/sharing/share-offsite/?url=${enc(SHARE_URL)}`,
closeOnSelect: true,
trailing: <ExternalIcon />,
});
list.push({
id: "facebook",
label: "Share on Facebook",
hint: "Post to your timeline",
icon: <FacebookIcon />,
accent:
"text-indigo-600 dark:text-indigo-300 bg-indigo-500/10 ring-indigo-500/20",
href: `https://www.facebook.com/sharer/sharer.php?u=${enc(SHARE_URL)}`,
closeOnSelect: true,
trailing: <ExternalIcon />,
});
list.push({
id: "email",
label: "Email a colleague",
hint: "Open your mail client",
icon: <EmailIcon />,
accent:
"text-amber-600 dark:text-amber-300 bg-amber-500/10 ring-amber-500/20",
href: `mailto:?subject=${enc(SHARE_TITLE)}&body=${enc(`${SHARE_TEXT}\n\n${SHARE_URL}`)}`,
closeOnSelect: true,
trailing: <ExternalIcon />,
});
return list;
}, [canNativeShare, copied, copyLink, nativeShare, enc]);
/* --- focus management on open --- */
useEffect(() => {
if (!open) return;
const idx = pendingFocus.current === "last" ? items.length - 1 : 0;
setActiveIndex(idx);
const raf = requestAnimationFrame(() => {
itemRefs.current[idx]?.focus();
});
return () => cancelAnimationFrame(raf);
}, [open, items.length]);
/* --- dismiss on outside pointer --- */
useEffect(() => {
if (!open) return;
function onPointerDown(event: PointerEvent) {
const target = event.target as Node | null;
if (!target) return;
if (
popoverRef.current?.contains(target) ||
triggerRef.current?.contains(target)
) {
return;
}
setOpen(false);
}
document.addEventListener("pointerdown", onPointerDown);
return () => document.removeEventListener("pointerdown", onPointerDown);
}, [open]);
/* --- roving focus helpers --- */
const focusIndex = useCallback((index: number) => {
setActiveIndex(index);
itemRefs.current[index]?.focus();
}, []);
const typeahead = useCallback(
(char: string): number => {
const lower = char.toLowerCase();
const count = items.length;
for (let step = 1; step <= count; step += 1) {
const i = (activeIndex + step) % count;
if (items[i].label.toLowerCase().startsWith(lower)) return i;
}
return -1;
},
[activeIndex, items],
);
const onMenuKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>) => {
const count = items.length;
switch (event.key) {
case "ArrowDown":
event.preventDefault();
focusIndex((activeIndex + 1) % count);
break;
case "ArrowUp":
event.preventDefault();
focusIndex((activeIndex - 1 + count) % count);
break;
case "Home":
event.preventDefault();
focusIndex(0);
break;
case "End":
event.preventDefault();
focusIndex(count - 1);
break;
case "Escape":
event.preventDefault();
closeMenu(true);
break;
case "Tab":
event.preventDefault();
closeMenu(true);
break;
default:
if (event.key.length === 1 && /\S/.test(event.key)) {
const found = typeahead(event.key);
if (found >= 0) {
event.preventDefault();
focusIndex(found);
}
}
}
},
[activeIndex, items.length, focusIndex, closeMenu, typeahead],
);
const onTriggerKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLButtonElement>) => {
if (event.key === "ArrowDown") {
event.preventDefault();
openMenu("first");
} else if (event.key === "ArrowUp") {
event.preventDefault();
openMenu("last");
}
},
[openMenu],
);
const onItemSelect = useCallback(
(item: ShareItem) => {
item.onSelect?.();
if (item.closeOnSelect) closeMenu(true);
},
[closeMenu],
);
const setItemRef =
(index: number) => (el: HTMLAnchorElement | HTMLButtonElement | null) => {
itemRefs.current[index] = el;
};
const itemClass =
"group/item flex w-full items-center gap-3 rounded-xl px-2.5 py-2 text-left outline-none transition-colors " +
"text-zinc-700 hover:bg-zinc-100 focus-visible:bg-zinc-100 " +
"dark:text-zinc-200 dark:hover:bg-white/[0.06] dark:focus-visible:bg-white/[0.06] " +
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 " +
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900 motion-reduce:transition-none";
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 sm:py-24 dark:from-zinc-950 dark:to-zinc-900">
<style>{`
@keyframes sharemenu-badge-in {
0% { opacity: 0; transform: scale(0.6); }
60% { transform: scale(1.08); }
100% { opacity: 1; transform: scale(1); }
}
.sharemenu-badge { animation: sharemenu-badge-in 260ms cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes sharemenu-dot {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
.sharemenu-live-dot { animation: sharemenu-dot 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.sharemenu-badge, .sharemenu-live-dot { animation: none; }
}
`}</style>
{/* soft backdrop glow */}
<div
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-0 h-72 w-[36rem] -translate-x-1/2 rounded-full bg-indigo-400/10 blur-3xl dark:bg-indigo-500/10"
/>
<div className="relative mx-auto w-full max-w-2xl">
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
Frontstack · Field guide
</p>
<article className="rounded-3xl border border-zinc-200 bg-white p-6 shadow-[0_1px_0_0_rgba(0,0,0,0.02),0_24px_48px_-24px_rgba(24,24,27,0.18)] sm:p-8 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-[0_24px_48px_-24px_rgba(0,0,0,0.6)]">
<div className="flex items-center gap-2 text-xs font-medium text-zinc-500 dark:text-zinc-400">
<span className="inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 font-semibold text-emerald-700 dark:text-emerald-300">
Accessibility
</span>
<span aria-hidden="true">·</span>
<span>7 min read</span>
</div>
<h2 className="mt-4 text-balance text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl dark:text-zinc-50">
{SHARE_TITLE}
</h2>
<p className="mt-3 text-pretty text-[15px] leading-relaxed text-zinc-600 dark:text-zinc-300">
A practical breakdown of the WAI-ARIA menu button: roving tabindex,
focus return to the trigger, first-character typeahead, and the exact
keyboard model users expect from a native control.
</p>
<div className="mt-6 flex items-center gap-3">
<span
aria-hidden="true"
className="grid h-10 w-10 place-items-center rounded-full bg-gradient-to-br from-indigo-500 to-violet-500 text-sm font-semibold text-white"
>
MO
</span>
<div className="text-sm">
<p className="font-medium text-zinc-900 dark:text-zinc-100">
Marisa Okafor
</p>
<p className="text-zinc-500 dark:text-zinc-400">
Updated Jul 15, 2026
</p>
</div>
</div>
<div className="my-6 h-px w-full bg-zinc-200 dark:bg-zinc-800" />
{/* --- action row with the share menu --- */}
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-zinc-500 dark:text-zinc-400">
Send this to a teammate.
</p>
<div className="relative">
<button
ref={triggerRef}
id={triggerId}
type="button"
aria-haspopup="menu"
aria-expanded={open}
aria-controls={open ? menuId : undefined}
onClick={() => (open ? closeMenu(false) : openMenu("first"))}
onKeyDown={onTriggerKeyDown}
className="inline-flex items-center gap-2 rounded-xl bg-zinc-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm outline-none transition-colors hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-white dark:focus-visible:ring-offset-zinc-900"
>
<ShareIcon />
Share
<span
className={`transition-transform duration-200 motion-reduce:transition-none ${
open ? "rotate-180" : "rotate-0"
}`}
>
<ChevronIcon />
</span>
</button>
<AnimatePresence>
{open && (
<motion.div
ref={popoverRef}
initial={{
opacity: 0,
y: reduce ? 0 : -6,
scale: reduce ? 1 : 0.96,
}}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{
opacity: 0,
y: reduce ? 0 : -6,
scale: reduce ? 1 : 0.97,
}}
transition={{
duration: reduce ? 0 : 0.16,
ease: [0.16, 1, 0.3, 1],
}}
style={{ transformOrigin: "top right" }}
className="absolute right-0 top-[calc(100%+0.5rem)] z-30 w-72 origin-top-right rounded-2xl border border-zinc-200 bg-white p-1.5 shadow-[0_24px_48px_-16px_rgba(24,24,27,0.25)] dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-[0_24px_48px_-16px_rgba(0,0,0,0.7)]"
>
<p
id={headingId}
className="px-2.5 pb-1.5 pt-2 text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500"
>
Share this article
</p>
<div
id={menuId}
role="menu"
aria-labelledby={headingId}
aria-orientation="vertical"
onKeyDown={onMenuKeyDown}
className="flex flex-col gap-0.5"
>
{items.map((item, index) => {
const inner = (
<>
<span
aria-hidden="true"
className={`grid h-9 w-9 shrink-0 place-items-center rounded-lg ring-1 ${item.accent}`}
>
{item.icon}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{item.label}
</span>
<span className="block truncate text-xs text-zinc-400 dark:text-zinc-500">
{item.hint}
</span>
</span>
{item.trailing ? (
<span className="shrink-0 text-zinc-400 dark:text-zinc-500">
{item.trailing}
</span>
) : null}
</>
);
if (item.href) {
return (
<a
key={item.id}
ref={setItemRef(index)}
href={item.href}
target="_blank"
rel="noopener noreferrer"
role="menuitem"
tabIndex={index === activeIndex ? 0 : -1}
onFocus={() => setActiveIndex(index)}
onMouseEnter={() => setActiveIndex(index)}
onClick={() => onItemSelect(item)}
className={itemClass}
>
{inner}
</a>
);
}
return (
<button
key={item.id}
ref={setItemRef(index)}
type="button"
role="menuitem"
tabIndex={index === activeIndex ? 0 : -1}
onFocus={() => setActiveIndex(index)}
onMouseEnter={() => setActiveIndex(index)}
onClick={() => onItemSelect(item)}
className={itemClass}
>
{inner}
</button>
);
})}
</div>
<p className="flex items-center gap-1.5 px-2.5 pb-1.5 pt-2 text-[11px] text-zinc-400 dark:text-zinc-500">
<span
aria-hidden="true"
className="sharemenu-live-dot inline-block h-1.5 w-1.5 rounded-full bg-emerald-500"
/>
Anyone with the link can read this article.
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* --- always-visible quick copy row --- */}
<div className="mt-5">
<label
htmlFor={linkFieldId}
className="mb-1.5 block text-xs font-medium text-zinc-500 dark:text-zinc-400"
>
Article link
</label>
<div className="flex items-stretch gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2 dark:border-zinc-700 dark:bg-zinc-800/60">
<span className="text-zinc-400 dark:text-zinc-500">
<LinkIcon />
</span>
<input
id={linkFieldId}
type="text"
readOnly
value={SHARE_URL}
onFocus={(event) => event.currentTarget.select()}
className="w-full min-w-0 truncate bg-transparent text-sm text-zinc-700 outline-none dark:text-zinc-200"
/>
</div>
<button
type="button"
onClick={copyLinkRow}
aria-label={rowCopied ? "Link copied" : "Copy article link"}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-xl px-3.5 py-2 text-sm font-semibold outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-reduce:transition-none dark:focus-visible:ring-offset-zinc-900 ${
rowCopied
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
: "border border-zinc-200 text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-white/[0.06]"
}`}
>
{rowCopied ? <CheckIcon /> : <CopyIcon />}
<span className={rowCopied ? "sharemenu-badge" : undefined}>
{rowCopied ? "Copied" : "Copy"}
</span>
</button>
</div>
</div>
</article>
</div>
{/* polite announcements for assistive tech */}
<div aria-live="polite" className="sr-only">
{announce}
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Dropdown Menu
Originalbasic dropdown menu with items
Dropdown Icons Menu
Originaldropdown with icons and shortcuts

User Menu
Originaluser account dropdown with avatar

Mega Menu
Originalmega menu with grouped links

Mega Cols Menu
Originalmulti-column mega menu with featured

Context Menu
Originalright-click context menu

Nested Menu
Originaldropdown with nested submenus

Select Menu
Originalcustom single select dropdown

Multiselect Menu
Originalmulti-select with checkable items

Command Menu
Originalcommand palette style menu with search

Actions Menu
Originalactions/kebab menu

Notifications Menu
Originalnotifications dropdown list

