Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
urlstringwindow.location.hrefThe link to share. Read at click time when omitted.
titlestringTitle passed to the share sheet.
textstringA short text passed to the share sheet.
labelstring"Share"Visible label, or the accessible name on an icon size.
variantButton variant"outline"Any shadcn button variant.
sizeButton 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