Share button
Opens the native share sheet, or copies the link and says so where there is none.
npx shadcn@latest add https://ui.goodtabs.co/r/share-button.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/share-button
Outline with the Share label. On a laptop it copies and shows Link copied.
Icon only
An icon size hides the label and moves it to the accessible name.
With a custom label
Your own label on the default and secondary variants.
Usage
import { ShareButton } from "@/components/ui/share-button"
<ShareButton title={post.title} text={post.summary} />
<ShareButton size="icon-sm" variant="ghost" url={post.url} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| url | string | window.location.href | The link to share. Read at click time when omitted. |
| title | string | Title passed to the share sheet. | |
| text | string | A short text passed to the share sheet. | |
| label | string | "Share" | Visible label, or the accessible name on an icon size. |
| variant | Button variant | "outline" | Any shadcn button variant. |
| size | Button size | "default" | Any size. icon, icon-sm and icon-xs render the icon only. |
Source
share-button/share-button.tsx
"use client";
import * as React from "react";
import { CheckIcon, ShareIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type ShareButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick" | "children"> & {
/** The link to share. Defaults to the current page when omitted. */
url?: string;
/** Title passed to the share sheet. */
title?: string;
/** A short text passed to the share sheet. */
text?: string;
/** Visible label, or the accessible name on an icon size. */
label?: string;
};
/**
* Opens the native share sheet where there is one, otherwise copies the link and says so.
* Icon only when size is one of the icon sizes; labelled otherwise.
*/
export function ShareButton({
url,
title,
text,
label = "Share",
variant = "outline",
size = "default",
className,
...props
}: ShareButtonProps) {
const [copied, setCopied] = React.useState(false);
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const iconOnly = typeof size === "string" && size.startsWith("icon");
React.useEffect(() => () => clearTimeout(timer.current), []);
const share = async () => {
const href = url ?? window.location.href;
const data: ShareData = { title, text, url: href };
if (typeof navigator.share === "function" && (!navigator.canShare || navigator.canShare(data))) {
try {
await navigator.share(data);
return;
} catch (error) {
// The person closed the sheet. Nothing to report.
if (error instanceof Error && error.name === "AbortError") return;
}
}
try {
await navigator.clipboard.writeText(href);
setCopied(true);
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard can be blocked (insecure context, permissions). Fail quietly.
}
};
const Icon = copied ? CheckIcon : ShareIcon;
return (
<Button
type="button"
variant={variant}
size={size}
aria-label={iconOnly ? label : undefined}
data-copied={copied || undefined}
onClick={share}
className={cn("transition-colors", className)}
{...props}
>
<Icon data-icon={iconOnly ? undefined : "inline-start"} aria-hidden="true" />
{iconOnly ? null : <span>{copied ? "Link copied" : label}</span>}
<span className="sr-only" aria-live="polite">
{copied ? "Link copied" : ""}
</span>
</Button>
);
}
For AI agents
Hand your agent this page, the markdown at /components/share-button/prompt.md, or the Copy AI prompt button above.
When to use
- On an article, resource or product page that people pass around
- In a card toolbar or page header next to save and copy
- When you want the native share sheet on phones without a second code path
When not to use
- For sharing with named people inside your app; build an invite flow
- When only the url needs copying and the sheet adds nothing; use the copy button
- For files or images; the Web Share API handles those differently
Agent notes
- It extends shadcn's Button, so every variant and size applies; onClick and children are reserved
- navigator.share needs a secure context and a user gesture, and most desktop browsers have no sheet, so the clipboard path is the common one there
- Link copied shows for two seconds in the label, the icon and an aria-live region; data-copied is set on the button meanwhile
- url defaults to the current page at click time, so it is safe in a server-rendered layout