bottega

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-card

Usage

Usagetsx
<SocialLinksCard />

Props

PropTypeDefaultDescription
headingstringSocialText shown on the overlay before hover (default "Social").
linksSocialLink[][ { 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.
classNamestringExtra 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