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-buttonsor 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
1"use client";2
3import React, { useState, useCallback } from "react";4import { ButtonGroup } from "./ButtonGroup";5
6interface ShareButtonsProps {7 url?: string;8 title?: string;9 className?: string;10}11
12
13function LinkIcon() {14 return (15 <svg16 width="14"17 height="14"18 viewBox="0 0 24 24"19 fill="none"20 stroke="currentColor"21 strokeWidth="1.75"22 strokeLinecap="round"23 strokeLinejoin="round"24 >25 <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />26 <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />27 </svg>28 );29}30
31function CheckIcon() {32 return (33 <svg34 width="14"35 height="14"36 viewBox="0 0 24 24"37 fill="none"38 stroke="currentColor"39 strokeWidth="2"40 strokeLinecap="round"41 strokeLinejoin="round"42 >43 <polyline points="20 6 9 17 4 12" />44 </svg>45 );46}47
48function XIcon() {49 return (50 <svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24">51 <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" />52 </svg>53 );54}55
56function LinkedInIcon() {57 return (58 <svg width="13" height="13" fill="currentColor" viewBox="0 0 24 24">59 <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" />60 </svg>61 );62}63
64export function ShareButtons({65 url,66 title,67 className = "",68}: ShareButtonsProps) {69 const [copied, setCopied] = useState(false);70
71 const shareUrl = url ?? (typeof window !== "undefined" ? window.location.href : "");72 const shareTitle = title ?? "";73
74 const handleCopy = useCallback(async () => {75 try {76 await navigator.clipboard.writeText(shareUrl);77 setCopied(true);78 setTimeout(() => setCopied(false), 2000);79 } catch {80 // Fallback silently81 }82 }, [shareUrl]);83
84 const handleTwitter = useCallback(() => {85 const params = new URLSearchParams({86 text: shareTitle,87 url: shareUrl,88 });89 window.open(90 `https://x.com/intent/tweet?${params.toString()}`,91 "_blank",92 "noopener,noreferrer"93 );94 }, [shareUrl, shareTitle]);95
96 const handleLinkedIn = useCallback(() => {97 const params = new URLSearchParams({98 url: shareUrl,99 });100 window.open(101 `https://www.linkedin.com/sharing/share-offsite/?${params.toString()}`,102 "_blank",103 "noopener,noreferrer"104 );105 }, [shareUrl]);106
107 const keys = [108 {109 label: copied ? "Link copied" : "Copy link",110 onClick: handleCopy as () => void,111 content: (112 <>113 <span key={String(copied)} className={["animate-checkbox-in", copied ? "text-emerald-600" : ""].join(" ")}>114 {copied ? <CheckIcon /> : <LinkIcon />}115 </span>116 <span className="w-[62px] text-left">{copied ? "copied" : "copy link"}</span>117 </>118 ),119 wide: true,120 },121 { label: "Share on X", onClick: handleTwitter, content: <XIcon />, wide: false },122 { label: "Share on LinkedIn", onClick: handleLinkedIn, content: <LinkedInIcon />, wide: false },123 ];124
125 return (126 <div className={className}>127 <p className="text-[12px] font-medium text-neutral-500 lowercase mb-3">128 Share129 </p>130 <ButtonGroup label="Share" items={keys} />131 <span aria-live="polite" className="sr-only">{copied ? "Link copied to clipboard" : ""}</span>132 </div>133 );134}components/ui/ButtonGroup.tsxtsx
1import React from "react";2
3export interface ButtonGroupItem {4 label: string;5 content: React.ReactNode;6 onClick?: () => void;7 href?: string;8 /** Sized to its content; otherwise a square key for a lone icon */9 wide?: boolean;10}11
12interface ButtonGroupProps {13 items: ButtonGroupItem[];14 label: string;15 className?: string;16}17
18/* One bezel, one face, several keys: a linked control in the same material19 as Button (sm), split by grooves rather than gaps. Each key fills from the20 bottom on hover and sinks when pressed, like a layered Button's face. */21export function ButtonGroup({ items, label, className = "" }: ButtonGroupProps) {22 return (23 <div24 role="group"25 aria-label={label}26 className={["inline-flex w-fit rounded-[12px] p-[3px]", className].join(" ")}27 style={{ background: "var(--bezel-light)", boxShadow: "var(--bezel-light-shadow)" }}28 >29 <div30 className="flex rounded-[9px] overflow-hidden"31 style={{ background: "var(--face-light)", boxShadow: "var(--face-light-shadow)" }}32 >33 {items.map((item, i) => {34 const keyClass = [35 "group/key relative h-9 inline-flex items-center justify-center gap-1.5 cursor-pointer",36 "text-[13px] font-medium text-neutral-800 outline-none",37 "transition-[box-shadow,background] duration-150",38 "active:bg-neutral-900/[0.04] active:[box-shadow:inset_0_1px_2px_rgba(17,17,17,0.08)]",39 "focus-visible:[box-shadow:inset_0_0_0_2px_rgba(17,17,17,0.85)]",40 item.wide ? "px-4" : "w-10",41 ].join(" ");42 const inner = (43 <>44 <span45 aria-hidden46 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"47 />48 <span className="relative inline-flex items-center gap-1.5">{item.content}</span>49 </>50 );51 const common = {52 "aria-label": item.label,53 title: item.wide ? undefined : item.label,54 className: keyClass,55 style: { textShadow: "0 1px 0 rgba(255,255,255,0.9)" },56 };57
58 return (59 <React.Fragment key={item.label}>60 {i > 0 && (61 <span62 aria-hidden63 className="w-px self-stretch my-2 bg-neutral-900/[0.08]"64 style={{ boxShadow: "1px 0 0 rgba(255,255,255,0.9)" }}65 />66 )}67 {item.href ? (68 <a href={item.href} {...common}>69 {inner}70 </a>71 ) : (72 <button type="button" onClick={item.onClick} {...common}>73 {inner}74 </button>75 )}76 </React.Fragment>77 );78 })}79 </div>80 </div>81 );82}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;}