Skip to content
goodtabs UI
BrowseBrowse components

Social links

A row of icon links to social profiles, each with an accessible name.

npx shadcn@latest add https://ui.goodtabs.co/r/social-links.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/social-links

Five ghost icon pills; the name is the aria-label.

With labels

showLabels puts the name next to each icon.

Outline, small

The outline variant at 28px.

Usage

import { SocialLinks } from "@/components/ui/social-links"

<SocialLinks
  links={[
    { name: "GitHub", href: "https://github.com/goodtabs", icon: <GitHubIcon /> },
    { name: "X", href: "https://x.com/goodtabs", icon: <XIcon /> },
  ]}
/>

Props

PropTypeDefaultDescription
links{ name: string; href: string; icon: ReactNode }[]The profiles, in order. name is the accessible name.
size"sm" | "default""default"28px or 32px tall.
showLabelsbooleanfalseShow the name next to the icon.
variant"ghost" | "outline""ghost"Ghost for footers, outline to stand alone.

Source

social-links/social-links.tsx
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

export type SocialLink = {
  /** The network or profile name, used as the accessible name and the visible label. */
  name: string;
  /** Profile url. */
  href: string;
  /** Any icon node, for example an inline SVG with fill currentColor. */
  icon: React.ReactNode;
};

type SocialLinksProps = Omit<React.ComponentProps<"nav">, "children"> & {
  /** The profiles, in order. */
  links: SocialLink[];
  /** 28px or 32px tall. */
  size?: "sm" | "default";
  /** Show the name next to the icon. Icon only otherwise, with the name as aria-label. */
  showLabels?: boolean;
  /** Ghost for footers and toolbars, outline to stand on its own. */
  variant?: "ghost" | "outline";
};

/**
 * A row of icon links to social profiles. Every link has an accessible name.
 * Icons come from you, so any network works and the file stays small.
 */
export function SocialLinks({
  links,
  size = "default",
  showLabels = false,
  variant = "ghost",
  className,
  ...props
}: SocialLinksProps) {
  const buttonSize = showLabels ? (size === "sm" ? "sm" : "default") : size === "sm" ? "icon-sm" : "icon";
  return (
    <nav aria-label="Social links" className={cn("flex items-center", className)} {...props}>
      <ul className="m-0 flex list-none items-center gap-1 p-0">
        {links.map((link) => (
          <li key={link.href}>
            <Button asChild variant={variant} size={buttonSize} className="rounded-full">
              <a
                href={link.href}
                target="_blank"
                rel="noreferrer"
                aria-label={showLabels ? undefined : link.name}
                className="text-muted-foreground"
              >
                <span data-icon={showLabels ? "inline-start" : undefined} className="contents [&_svg]:size-4">
                  {link.icon}
                </span>
                {showLabels && <span>{link.name}</span>}
              </a>
            </Button>
          </li>
        ))}
      </ul>
    </nav>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/social-links/prompt.md, or the Copy AI prompt button above.

When to use

  • In a footer or an about page, under the name or logo
  • On a profile or team card where two to six networks are listed
  • Next to a newsletter signup as the other way to follow along

When not to use

  • For share buttons, which need the current url and a different action
  • For in-app navigation; these open new tabs
  • With more than about six links, where a list with labels reads better

Agent notes

  • It ships no icons: pass inline SVGs with fill currentColor, since lucide has no brand marks; the demos include X, GitHub, LinkedIn, YouTube and Bluesky
  • Every link opens in a new tab with rel noreferrer; the name is the aria-label when labels are hidden
  • Icons are muted-foreground and turn foreground on hover through the Button variant
  • Server component; the nav takes any nav props, including its own aria-label