Skip to main content
Urbicon UI
Back to Recipes

Clickable Card

Card as one fully clickable element — dashboard tile, list card, navigation tile. Card automatically renders as <a> or <button> depending on the prop; nested <a><Card></Card></a> is unnecessary (and usually wrong).

Live Preview

List card with action (onclick)

Features

  • href automatically sets the rendered element to <a> with correct tab order
  • onclick sets it to <button type="button"> for actions without navigation
  • Hover/focus styles kick in automatically once href/onclick/clickable is set — no "decorative hover" mode on passive elements
  • mint="translate" or mint="glow" for a gentle micro-interaction
  • Anti-pattern comparison: why <a><Card></Card></a> is the worse solution

Code

Stat tile with href (renders as <a>)

<Card variant="outlined" padding="md" href="/revenue" mint="translate">
  <div class="flex items-start justify-between">
    <div>
      <p class="text-text-tertiary text-xs font-medium">Revenue</p>
      <p class="text-text-primary text-2xl font-semibold tabular-nums">€42,150</p>
      <p class="text-success text-xs">+12.4% vs. last month</p>
    </div>
    <ArrowRightIcon class="text-text-tertiary h-4 w-4" />
  </div>
</Card>

List card with onclick (renders as <button>)

<Card
  variant="outlined"
  padding="sm"
  onclick={() => navigateTo(user.id)}
>
  <div class="flex items-center gap-3">
    <Avatar name={user.name} size="sm" />
    <div class="min-w-0 flex-1">
      <p class="text-text-primary text-sm font-medium">{user.name}</p>
      <p class="text-text-tertiary text-xs">{user.role}</p>
    </div>
  </div>
</Card>

Delegate click without onclick (clickable prop)

<!-- Render Card as a <button> without it carrying a handler itself —
     for patterns where a wrapper component intercepts clicks (e.g. a
     selection picker bound across multiple Card children).
     Don't nest it inside an outer <a> — that produces
     <a><button> nesting (an a11y violation). -->
<Card variant="outlined" padding="md" clickable>
  ...
</Card>

Anti-pattern: wrapping Card in <a>

Don't

<a href="/revenue">
  <Card variant="outlined" padding="md">
    Content
  </Card>
</a>
  • Card keeps elementType="div" — no hover/focus styles
  • Inner inputs/links produce nested interactive elements (a11y violation)
  • Two tab stops (outer a + card content) instead of one
  • Screen readers announce "link, region, …" twice

Do

<Card
  variant="outlined"
  padding="md"
  href="/revenue"
>
  Content
</Card>
  • Card renders directly as <a>
  • Hover/focus styles apply automatically
  • One tab stop, one element for screen readers
  • No conflicts with inner buttons (stop bubbling where needed)

Best Practices

href for navigation, onclick for actions

Routes, detail pages → href. Opening a modal, changing a selection, server actions → onclick. That way Cmd-click "open in new tab" works as expected.

Inner buttons: stopPropagation

If the card contains a secondary button (e.g. "Favorite"), end the button's onclick with event.stopPropagation() — otherwise the card fires too.

mint="translate" for a gentle hover animation

mint="translate" lifts the card slightly on hover; mint="glow" adds a subtle glow. prefers-reduced-motion is respected.

clickable for delegated click handlers

When click behavior is handled by a wrapper component and the card should render as a real button without carrying an onclick itself — set clickable without href/onclick. The card then renders as <button type="button"> with interactive styles. Don't combine it with an outer <a> or inner buttons — <a><Card clickable> produces nested interactive elements.

No "decorative hover"

Card deliberately has no mode with hover styles but no click source. A pointer cursor plus a lift animation on a passive element violates WCAG 3.2 Predictable. If the card sits inside an outer <a>, move the href onto the card (see the anti-pattern section).