editorialclient

share buttons

share a post to social sites, or copy its link.

Share

installation

new to blurrow? this sets up your project in one go: the package, the styles and the font. already done it? skip straight to usage. the installation page has the details.

bash
npx @blurrow/ui init
›rather own the code? copy the source instead

let the cli copy it, along with anything it needs:

bash
npx @blurrow/ui add share-buttons

or by hand: drop these files into components/ui and import from there. share buttons leans on ButtonGroup.tsx, so those come along too.

components/ui/ShareButtons.tsxtsx
"use client";
import React, { useState, useCallback } from "react";
import { ButtonGroup } from "./ButtonGroup";
interface ShareButtonsProps {
url?: string;
title?: string;
className?: string;
}
function LinkIcon() {
return (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
);
}
function CheckIcon() {
return (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
);
}
function XIcon() {
return (
<svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
function LinkedInIcon() {
return (
<svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
);
}
export function ShareButtons({
url,
title,
className = "",
}: ShareButtonsProps) {
const [copied, setCopied] = useState(false);
const shareUrl = url ?? (typeof window !== "undefined" ? window.location.href : "");
const shareTitle = title ?? "";
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(shareUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback silently
}
}, [shareUrl]);
const handleTwitter = useCallback(() => {
const params = new URLSearchParams({
text: shareTitle,
url: shareUrl,
});
window.open(
`https://x.com/intent/tweet?${params.toString()}`,
"_blank",
"noopener,noreferrer"
);
}, [shareUrl, shareTitle]);
const handleLinkedIn = useCallback(() => {
const params = new URLSearchParams({
url: shareUrl,
});
window.open(
`https://www.linkedin.com/sharing/share-offsite/?${params.toString()}`,
"_blank",
"noopener,noreferrer"
);
}, [shareUrl]);
const keys = [
{
label: copied ? "Link copied" : "Copy link",
onClick: handleCopy as () => void,
content: (
<>
<span key={String(copied)} className={["animate-checkbox-in", copied ? "text-emerald-600" : ""].join(" ")}>
{copied ? <CheckIcon /> : <LinkIcon />}
</span>
<span className="w-[62px] text-left">{copied ? "copied" : "copy link"}</span>
</>
),
wide: true,
},
{ label: "Share on X", onClick: handleTwitter, content: <XIcon />, wide: false },
{ label: "Share on LinkedIn", onClick: handleLinkedIn, content: <LinkedInIcon />, wide: false },
];
return (
<div className={className}>
<p className="text-[12px] font-medium text-neutral-500 lowercase mb-3">
Share
</p>
<ButtonGroup label="Share" items={keys} />
<span aria-live="polite" className="sr-only">{copied ? "Link copied to clipboard" : ""}</span>
</div>
);
}
components/ui/ButtonGroup.tsxtsx
import React from "react";
export interface ButtonGroupItem {
label: string;
content: React.ReactNode;
onClick?: () => void;
href?: string;
/** Sized to its content; otherwise a square key for a lone icon */
wide?: boolean;
}
interface ButtonGroupProps {
items: ButtonGroupItem[];
label: string;
className?: string;
}
/* One bezel, one face, several keys: a linked control in the same material
as Button (sm), split by grooves rather than gaps. Each key fills from the
bottom on hover and sinks when pressed, like a layered Button's face. */
export function ButtonGroup({ items, label, className = "" }: ButtonGroupProps) {
return (
<div
role="group"
aria-label={label}
className={["inline-flex w-fit rounded-[12px] p-[3px]", className].join(" ")}
style={{ background: "var(--bezel-light)", boxShadow: "var(--bezel-light-shadow)" }}
>
<div
className="flex rounded-[9px] overflow-hidden"
style={{ background: "var(--face-light)", boxShadow: "var(--face-light-shadow)" }}
>
{items.map((item, i) => {
const keyClass = [
"group/key relative h-9 inline-flex items-center justify-center gap-1.5 cursor-pointer",
"text-[13px] font-medium text-neutral-800 outline-none",
"transition-[box-shadow,background] duration-150",
"active:bg-neutral-900/[0.04] active:[box-shadow:inset_0_1px_2px_rgba(17,17,17,0.08)]",
"focus-visible:[box-shadow:inset_0_0_0_2px_rgba(17,17,17,0.85)]",
item.wide ? "px-4" : "w-10",
].join(" ");
const inner = (
<>
<span
aria-hidden
className="pointer-events-none absolute inset-0 origin-bottom scale-y-0 bg-neutral-900/[0.025] transition-transform duration-300 ease-out group-hover/key:scale-y-100"
/>
<span className="relative inline-flex items-center gap-1.5">{item.content}</span>
</>
);
const common = {
"aria-label": item.label,
title: item.wide ? undefined : item.label,
className: keyClass,
style: { textShadow: "0 1px 0 rgba(255,255,255,0.9)" },
};
return (
<React.Fragment key={item.label}>
{i > 0 && (
<span
aria-hidden
className="w-px self-stretch my-2 bg-neutral-900/[0.08]"
style={{ boxShadow: "1px 0 0 rgba(255,255,255,0.9)" }}
/>
)}
{item.href ? (
<a href={item.href} {...common}>
{inner}
</a>
) : (
<button type="button" onClick={item.onClick} {...common}>
{inner}
</button>
)}
</React.Fragment>
);
})}
</div>
</div>
);
}

usage

tsx
import { ShareButtons } from "@blurrow/ui";
tsx
<ShareButtons url={url} title={title} />

props

straight from the source, so they're always up to date.

propsts
interface ShareButtonsProps {
url?: string;
title?: string;
className?: string;
}