Share Bar
alphav0.2.0Social share button row — nine built-in platforms, custom targets, and copy-link with success feedback.
Context
Reach for it on news article footers, blog post footers, product page social rows, video player share clusters, doc page share affordances. Built-in URL templates for Twitter / Facebook / LinkedIn / Reddit / WhatsApp / Telegram / Email / Threads / Bluesky. Copy-link button uses navigator.clipboard with execCommand fallback for older / insecure-context browsers; success/error feedback is visual (icon flip) + audible (aria-live). Custom targets via 'kind: custom' with arbitrary onClick. Analytics hook via onShare(targetKind). SSR-safe — window.location.href read at click time, not render time.
Installation
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/share-barAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/share-bar-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
Share
Click Copy link — icon flips to a check for 2s.
Demo source
"use client"; import { useState } from "react";import { Send } from "lucide-react";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { ShareBar } from "./share-bar";import { SHARE_BAR_DUMMY_COMPACT, SHARE_BAR_DUMMY_DEFAULT, SHARE_BAR_DUMMY_FULL, SHARE_BAR_DUMMY_TITLE, SHARE_BAR_DUMMY_TR, SHARE_BAR_DUMMY_URL,} from "./dummy-data";import type { ShareTarget } from "./types"; export default function ShareBarDemo() { const [lastShared, setLastShared] = useState<string | null>(null); const [copyEvents, setCopyEvents] = useState(0); const [internalDialog, setInternalDialog] = useState<string | null>(null); const customTargets: ReadonlyArray<ShareTarget> = [ { kind: "twitter" }, { kind: "linkedin" }, { kind: "custom", id: "send-to-teammate", icon: Send, ariaLabel: "Send to teammate", onClick: () => setInternalDialog("Internal share dialog opened (demo)"), }, { kind: "copy" }, ]; return ( <Tabs defaultValue="default" className="w-full"> <SwipeTabsList> <TabsTrigger value="default">Default</TabsTrigger> <TabsTrigger value="full">Full + analytics</TabsTrigger> <TabsTrigger value="custom">Custom target</TabsTrigger> <TabsTrigger value="compact">Compact</TabsTrigger> <TabsTrigger value="i18n">Localized (TR)</TabsTrigger> </SwipeTabsList> <TabsContent value="default" className="mt-6 max-w-md"> <ShareBar targets={SHARE_BAR_DUMMY_DEFAULT} url={SHARE_BAR_DUMMY_URL} title={SHARE_BAR_DUMMY_TITLE} headingAs="h4" divider /> <p className="mt-3 text-xs text-muted-foreground"> Click <strong>Copy link</strong> — icon flips to a check for 2s. </p> </TabsContent> <TabsContent value="full" className="mt-6 max-w-2xl"> <ShareBar targets={SHARE_BAR_DUMMY_FULL} url={SHARE_BAR_DUMMY_URL} title={SHARE_BAR_DUMMY_TITLE} via="ilinxa_news" hashtags={["ilinxa", "shadcn", "react"]} headingAs="h4" divider onShare={(target) => setLastShared(target)} onCopySuccess={() => setCopyEvents((n) => n + 1)} /> <p className="mt-3 text-xs text-muted-foreground"> {lastShared ? <>Last shared: <code>{lastShared}</code></> : <>Click any button to log via <code>onShare</code>.</>} </p> <p className="mt-1 text-xs text-muted-foreground"> Copy successes (analytics): <code>{copyEvents}</code> — fires only on successful clipboard write, distinct from <code>onShare</code> which fires on every share-target click. </p> </TabsContent> <TabsContent value="custom" className="mt-6 max-w-md"> <ShareBar targets={customTargets} url={SHARE_BAR_DUMMY_URL} title={SHARE_BAR_DUMMY_TITLE} /> <p className="mt-3 text-xs text-muted-foreground"> Custom target with <code>onClick</code>:{" "} {internalDialog ? ( <span className="text-primary">{internalDialog}</span> ) : ( <span>click the paper-plane icon.</span> )} </p> </TabsContent> <TabsContent value="compact" className="mt-6 max-w-md"> <ShareBar targets={SHARE_BAR_DUMMY_COMPACT} url={SHARE_BAR_DUMMY_URL} title={SHARE_BAR_DUMMY_TITLE} /> </TabsContent> <TabsContent value="i18n" className="mt-6 max-w-md"> <ShareBar targets={SHARE_BAR_DUMMY_TR} url={SHARE_BAR_DUMMY_URL} title="ShareBar — pro-ui paylaşım çubuğu" headingAs="h4" divider labels={{ heading: "Paylaş", copyAria: "Bağlantıyı kopyala", copySuccess: "Bağlantı kopyalandı", copyError: "Bağlantı kopyalanamadı", }} /> </TabsContent> </Tabs> );} Usage
When to use
A horizontal cluster of social-share buttons + a copy-link button, typically at the bottom of an article (or pinned to the side as a share rail). Each social button opens the platform's share intent in a new window with the page's URL prefilled; the copy button writes the URL to the clipboard with a 2-second visual + audible success affordance.
Basic example
import { ShareBar } from "@/components/share-bar"
export function ArticleFooter() {
return (
<ShareBar
url="https://example.com/news/sustainable-cities"
title="Sustainable cities, then and now"
headingAs="h4"
divider
targets={[
{ kind: "twitter" },
{ kind: "facebook" },
{ kind: "linkedin" },
{ kind: "copy" },
]}
/>
)
}Built-in platforms
Twitter / Facebook / LinkedIn / Reddit / WhatsApp / Telegram / Email / Threads / Bluesky — each ships with a share-intent URL template + a default Lucide icon + a default English aria-label. Override the icon or aria-label per target if you want a different look or different text.
Custom targets
import { Send } from "lucide-react"
<ShareBar
url={article.canonicalUrl}
targets={[
{ kind: "twitter" },
{ kind: "linkedin" },
{
kind: "custom",
id: "send-to-teammate",
icon: Send,
ariaLabel: "Send to teammate",
onClick: () => openInternalShareDialog(article.id),
},
{ kind: "copy" },
]}
/>Analytics hook
<ShareBar
targets={[{ kind: "twitter" }, { kind: "facebook" }, { kind: "copy" }]}
onShare={(target) => analytics.track("article.share", { id: article.id, target })}
/>onShare(targetKind) fires after a successful share-intent open or successful copy. For granular hooks use onCopySuccess / onCopyError.
URL resolution
- If
urlis provided, that's what gets shared. - Otherwise,
window.location.hrefis read at click time on the client — never during render. SSR-safe. - Pass
title/text/via/hashtagsfor templates that accept them (Twitter, Email, WhatsApp, etc.).
Copy fallback
Copy uses navigator.clipboard.writeTextfirst; if that isn't available (older browsers, insecure HTTP context, embedded webviews), it falls back to the deprecated document.execCommand("copy") on a hidden textarea. Both paths are inside a try/catch so the error state surfaces to onCopyError + the icon flip.
Notes
- External links open in a new window with
noopener,noreferrerfor safety. - Copy success/error feedback is dual-channel: visible icon flip +
aria-liveannouncement (politefor success,alertfor errors). - The 2-second success-reset timeout is cleared on unmount — no
setState after unmountwarning. - Threads + Bluesky icons fall back to
Share2from lucide-react until first-party icons land. Override viatarget.iconif you ship custom SVGs. - The component is exported as
React.memo. Memoize thetargetsarray (or use a module-level constant) for the memo to bite — inline arrays defeat it. - Don't reach for this if you need share-count metrics — pure action surface, no API calls.
Features
- 9 built-in platforms with URL templates (Twitter / Facebook / LinkedIn / Reddit / WhatsApp / Telegram / Email / Threads / Bluesky)
- Copy-link button with success/error feedback (icon flip + aria-live)
- Clipboard fallback to document.execCommand for older / insecure browsers
- Custom targets via 'kind: custom' (arbitrary icon + onClick)
- Analytics hook via onShare(targetKind)
- Configurable URL / title / text / via / hashtags
- Optional section heading (h2 / h3 / h4) with i18n labels
- Optional top divider (`pt-8 border-t border-border`)
- External links use target=_blank rel=noopener,noreferrer
- Memoized; SSR-safe; <ul>/<li> semantics