cards
Social Links Card
A slim pill card with a split `--primary` overlay that covers the icon row at rest and reveals it on hover by sliding the two halves off (up/down). Each icon anchor floods with `--primary` on individual hover and bounces via a CSS keyframe. Heading slot shows through the overlay. Fully token-driven — no brand colors. Pure CSS hover, RSC-safe.
Ported from dovatgabriel (MIT)
Install
1. Register the namespace (once per project):
json
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
bash
npx shadcn add @bottega/social-links-cardUsage
Usagetsx
<SocialLinksCard />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| heading | string | Social | Text shown on the overlay before hover (default "Social"). |
| links | SocialLink[] | [ { href: "#", label: "GitHub", icon: <GitHubIcon /> }, { href: "#", label: "YouTube", icon: <YouTubeIcon /> }, { href: "#", label: "Instagram", icon: <InstagramIcon /> }, { href: "#", label: "X", icon: <XIcon /> }, ] | Social link items. Defaults to GitHub / YouTube / Instagram / X. |
| className | string | Extra class names merged onto the root element. |
Source
social-links-card.tsxtsx
import type React from "react";
import styles from "./social-links-card.module.css";
// --- Icon components (inline SVG, fill="currentColor", no brand colors) ---
function GitHubIcon() {
return (
<svg
className={styles.iconSvg}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fill="currentColor"
d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"
/>
</svg>
);
}
function YouTubeIcon() {
return (
<svg
className={styles.iconSvg}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{/* play circle cutout — single fill, no brand color */}
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M0 12C0 5.373 5.373 0 12 0s12 5.373 12 12-5.373 12-12 12S0 18.627 0 12zm10-5.5 8 5.5-8 5.5V6.5z"
/>
</svg>
);
}
function InstagramIcon() {
return (
<svg
className={styles.iconSvg}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
d="M12 3c-2.444 0-2.75.01-3.71.054-.959.044-1.613.196-2.185.418a4.412 4.412 0 0 0-1.595 1.038A4.412 4.412 0 0 0 3.472 6.1c-.222.572-.374 1.226-.418 2.185C3.01 9.25 3 9.556 3 12s.01 2.75.054 3.71c.044.959.196 1.613.418 2.185.23.592.538 1.094 1.038 1.595.5.5 1.003.809 1.595 1.038.572.222 1.226.374 2.185.418C9.25 20.99 9.556 21 12 21s2.75-.01 3.71-.054c.959-.044 1.613-.196 2.185-.418a4.412 4.412 0 0 0 1.595-1.038 4.412 4.412 0 0 0 1.038-1.595c.222-.572.374-1.226.418-2.185C20.99 14.75 21 14.444 21 12s-.01-2.75-.054-3.71c-.044-.959-.196-1.613-.418-2.185a4.412 4.412 0 0 0-1.038-1.595A4.412 4.412 0 0 0 17.9 3.472c-.572-.222-1.226-.374-2.185-.418C14.75 3.01 14.444 3 12 3zm0 1.622c2.403 0 2.688.009 3.637.052.877.04 1.354.187 1.671.31.42.163.72.358 1.035.673.315.315.51.615.673 1.035.123.317.27.794.31 1.671.043.95.052 1.234.052 3.637s-.009 2.688-.052 3.637c-.04.877-.187 1.354-.31 1.671a2.788 2.788 0 0 1-.673 1.035 2.788 2.788 0 0 1-1.035.673c-.317.123-.794.27-1.671.31-.95.043-1.234.052-3.637.052s-2.688-.009-3.637-.052c-.877-.04-1.354-.187-1.671-.31a2.788 2.788 0 0 1-1.035-.673 2.788 2.788 0 0 1-.673-1.035c-.123-.317-.27-.794-.31-1.671C4.631 14.688 4.622 14.403 4.622 12s.009-2.688.052-3.637c.04-.877.187-1.354.31-1.671.163-.42.358-.72.673-1.035a2.788 2.788 0 0 1 1.035-.673c.317-.123.794-.27 1.671-.31.95-.043 1.234-.052 3.637-.052zm0 2.76a4.618 4.618 0 1 0 0 9.236 4.618 4.618 0 0 0 0-9.236zm0 7.614a2.996 2.996 0 1 1 0-5.992 2.996 2.996 0 0 1 0 5.992zm5.406-8.707a1.08 1.08 0 1 1-2.16 0 1.08 1.08 0 0 1 2.16 0z"
/>
</svg>
);
}
function XIcon() {
return (
<svg
className={styles.iconSvg}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fill="currentColor"
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.744l7.73-8.835L1.254 2.25H8.08l4.259 5.622L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"
/>
</svg>
);
}
// --- Types ---
export type SocialLink = {
/** Link destination URL. */
href: string;
/** Accessible label (shown as aria-label on the anchor). */
label: string;
/** Icon node — use inline SVG with fill="currentColor" so tokens apply. */
icon: React.ReactNode;
};
const defaultLinks: SocialLink[] = [
{ href: "#", label: "GitHub", icon: <GitHubIcon /> },
{ href: "#", label: "YouTube", icon: <YouTubeIcon /> },
{ href: "#", label: "Instagram", icon: <InstagramIcon /> },
{ href: "#", label: "X", icon: <XIcon /> },
];
export type SocialLinksCardProps = {
/** Text shown on the overlay before hover (default "Social"). */
heading?: string;
/** Social link items. Defaults to GitHub / YouTube / Instagram / X. */
links?: SocialLink[];
/** Extra class names merged onto the root element. */
className?: string;
};
/**
* SocialLinksCard — a slim pill card with a split `--primary` overlay
* that reveals icon links on hover. Each icon floods with `--primary` on
* individual hover and bounces via a CSS keyframe. Pure CSS — RSC-safe.
*
* Ported from https://uiverse.io/dovatgabriel/pretty-fireant-44 (MIT).
* Brand colors replaced with `--primary` / `--primary-foreground` tokens.
*/
export function SocialLinksCard({
heading = "Social",
links = defaultLinks,
className = "",
}: SocialLinksCardProps) {
return (
<div
className={[styles.card, className].filter(Boolean).join(" ")}
role="group"
aria-label={`${heading} links`}
>
{/* Overlay label — aria-hidden; the individual links carry accessible names */}
<span className={styles.label} aria-hidden="true">
{heading}
</span>
{links.map((link) => (
<a
key={link.label}
className={styles.socialLink}
href={link.href}
aria-label={link.label}
>
{link.icon}
</a>
))}
</div>
);
}
Dependencies
- @bottega/tokens