Skip to content
goodtabs UI
BrowseBrowse components

Link card

A card that is one link, with a title, a line of text and an arrow top right.

npx shadcn@latest add https://ui.goodtabs.co/r/link-card.json

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

One card. Hover fills it softly and darkens the arrow; focus rings the card.

A grid of four

External

Opens in a new tab, with rel set and a note for screen readers.

Usage

import { LinkCard } from "@/components/ui/link-card"

<LinkCard href="/docs/installation" title="Installation" description="Add the registry and install any item with one command." />

Props

PropTypeDefaultDescription
hrefstring | UrlObjectWhere the card goes. Passed to next/link.
titlestringThe card heading.
descriptionstringOne or two lines under the title.
iconReactNodeOptional 16px icon above the title.
externalbooleanfalseOpens in a new tab with rel="noopener noreferrer" and a sr-only note.

Source

link-card/link-card.tsx
import * as React from "react";
import Link from "next/link";
import { ArrowUpRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";

type LinkCardProps = Omit<React.ComponentProps<typeof Link>, "title" | "children"> & {
  /** The card heading. */
  title: string;
  /** One or two lines under the title. */
  description?: string;
  /** Optional icon above the title, 16px, foreground. */
  icon?: React.ReactNode;
  /** Open in a new tab with rel="noopener noreferrer" and a screen reader note. */
  external?: boolean;
};

/**
 * A card that is one link. Title, description and an arrow top right.
 * The arrow and a soft fill are the hover signal; focus draws a ring around the whole card.
 */
export function LinkCard({ title, description, icon, external = false, className, ...props }: LinkCardProps) {
  return (
    <Link
      data-slot="link-card"
      target={external ? "_blank" : undefined}
      rel={external ? "noopener noreferrer" : undefined}
      className={cn(
        "group/link-card relative flex flex-col gap-1 rounded-lg border bg-card p-4 text-sm transition-colors duration-100 ease-out outline-none hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring/50",
        className,
      )}
      {...props}
    >
      <ArrowUpRightIcon
        aria-hidden="true"
        className="absolute top-3 right-3 size-4 text-muted-foreground transition-colors duration-100 ease-out group-hover/link-card:text-foreground group-focus-visible/link-card:text-foreground"
      />
      {icon && (
        <span aria-hidden="true" className="mb-2 inline-flex text-foreground [&_svg]:size-4">
          {icon}
        </span>
      )}
      <span className="pr-6 font-medium text-foreground">{title}</span>
      {description && <span className="pr-6 text-muted-foreground">{description}</span>}
      {external && <span className="sr-only">(opens in a new tab)</span>}
    </Link>
  );
}

For AI agents

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

When to use

  • A grid of destinations on a docs index or an overview page
  • Next steps at the end of a guide, two to four cards wide
  • Anywhere a whole surface should be one click, not a card with a button inside

When not to use

  • When the card holds other interactive things; nested links and buttons are invalid HTML
  • For a short inline link in text; use a plain link or the arrow link
  • For content that is not navigation, such as a stat or a notice

Agent notes

  • Server component built on next/link, so it needs a Next.js app and a real href; it accepts every Link prop
  • The whole card is the anchor: do not put buttons or other links inside it
  • Hover is bg-accent/40 and the arrow turns foreground; focus-visible rings the card, there is no other colour
  • It has no fixed width: set max-w on the card or put several in a CSS grid