# Link cards (themes.dev UI block)

Cards that each lead to a collection: icon, title, one line and an arrow that moves on hover; the whole card is one link. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

74% of the 2,073 sites in The UI Index have a card (1,540 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/ui/r/link-card.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/ui/r/astro/link-card.json`
- HTML: https://www.themes.dev/ui/r/html/link-card.html (with https://www.themes.dev/ui/r/html/caseroom.css)

Page: https://www.themes.dev/ui/blocks/link-card/ · Rules for agents: https://www.themes.dev/ui/AGENT.md

## Code

### link-card.astro

```astro
---
import Icon from '@/components/ui/icon.astro';
import { cn } from '@/lib/utils';
import { linkCard as c, type LinkCardContent } from './link-card.classes';

type Props = LinkCardContent & { class?: string };
const { id = 'link-card', label, title, items, class: className } = Astro.props;
---

<section class={cn(c.root, className)} id={id} aria-label={title ? undefined : label} aria-labelledby={title ? `${id}-title` : undefined}>
  <div class={c.inner}>
    {title && <h2 class={c.title} id={`${id}-title`}>{title}</h2>}
    <ul class={c.list}>
      {
        items.map((it) => (
          <li class={c.item}>
            <a href={it.href} class={c.card}>
              <span class={c.icon}><Icon name={it.icon} class={c.glyph} /></span>
              <span class={c.body}>
                <span class={c.name}>{it.title}</span>
                <span class={c.text}>{it.text}</span>
              </span>
              <Icon name="arrow-right" class={c.arrow} />
            </a>
          </li>
        ))
      }
    </ul>
  </div>
</section>
```

### link-card.classes.ts

```tsx
import type { IconName } from '@/components/ui/icon/icon.variants';

/** Link cards: a quiet row of cards, each one link to a collection (a topic, a category, a guide). The icon sits in
 *  a small square, then the title and one line, and the arrow at the end moves a little on hover and focus. The
 *  whole card is the one link, so there is no empty plate: every card has its icon, title and line. The arrow
 *  holds still under reduced motion. */
export const linkCard = {
  root: '@container bg-background px-6 py-14 text-foreground',
  inner: 'mx-auto max-w-6xl',
  title: 'mb-8 text-2xl font-semibold tracking-[-0.02em] text-balance',
  list: 'grid list-none gap-3 p-0 @lg:grid-cols-2 @4xl:grid-cols-3',
  item: 'flex',
  card: 'group flex min-h-14 w-full items-center gap-3 rounded-xl border bg-card p-3 text-card-foreground no-underline transition-[border-color,box-shadow,background-color] hover:border-foreground/30 hover:shadow-md focus-visible:border-foreground/30 active:bg-pressed/40 @xs:gap-4 @xs:p-4',
  icon: 'grid size-10 shrink-0 place-items-center rounded-lg bg-muted text-foreground',
  glyph: 'size-5',
  body: 'flex min-w-0 flex-1 flex-col gap-0.5',
  name: 'text-base font-semibold leading-snug text-balance',
  text: 'text-sm leading-snug text-muted-foreground',
  arrow: 'shrink-0 text-muted-foreground transition-[transform,color] duration-200 group-hover:translate-x-1 group-hover:text-foreground group-focus-visible:translate-x-1 group-focus-visible:text-foreground motion-reduce:transition-none motion-reduce:group-hover:translate-x-0 motion-reduce:group-focus-visible:translate-x-0',
};

export type LinkCardItem = { title: string; text: string; href: string; icon: IconName };
export type LinkCardContent = { id?: string; label: string; title?: string; items: LinkCardItem[] };
```

### link-card.tsx

```tsx
import { Icon } from '@/components/ui/icon';
import { cn } from '@/lib/utils';
import { linkCard as c, type LinkCardContent } from './link-card.classes';

export type LinkCardProps = LinkCardContent & { className?: string };

/** Link cards: the whole card is one link (icon, title, one line, an arrow that moves on hover and focus). */
export function LinkCard({ id = 'link-card', label, title, items, className }: LinkCardProps) {
  return (
    <section className={cn(c.root, className)} id={id} aria-label={title ? undefined : label} aria-labelledby={title ? `${id}-title` : undefined}>
      <div className={c.inner}>
        {title && <h2 className={c.title} id={`${id}-title`}>{title}</h2>}
        <ul className={c.list}>
          {items.map((it) => (
            <li key={it.href} className={c.item}>
              <a href={it.href} className={c.card}>
                <span className={c.icon}><Icon name={it.icon} className={c.glyph} /></span>
                <span className={c.body}>
                  <span className={c.name}>{it.title}</span>
                  <span className={c.text}>{it.text}</span>
                </span>
                <Icon name="arrow-right" className={c.arrow} />
              </a>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}
```
