Skip to main content
Urbicon UI
Back to Recipes

Clickable Card

Dashboard tiles that navigate and a team list whose cards act: href renders a Card as an anchor, onclick as a button, and hover, focus and mint styles arrive only with a real click source. A side-by-side comparison covers why the href belongs on the Card, not on a wrapper around it.

Built with Card Avatar

Live preview

DashboardTiles.svelte

Each tile renders as one <a>: hover lifts it, and Tab reaches it as a single stop.
<script lang="ts">
  import { ArrowRightIcon, BuildingIcon, Card } from '@urbicon-ui/blocks';
</script>

<!-- The width cap fits the docs stage — size the grid in your dashboard
     layout. The demo's links are fragments; your tiles take routes. -->
<div class="grid w-full max-w-2xl gap-4 sm:grid-cols-2">
  <Card variant="elevated" 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 mt-1 text-2xl font-semibold tabular-nums">€42,150</p>
        <p class="text-success mt-1 text-xs">+12.4% vs. last month</p>
      </div>
      <ArrowRightIcon size={16} class="text-text-tertiary" />
    </div>
  </Card>
  <Card variant="elevated" padding="md" href="#buildings" mint="translate">
    <div class="flex items-start justify-between">
      <div>
        <p class="text-text-tertiary text-xs font-medium">Properties</p>
        <p class="text-text-primary mt-1 text-2xl font-semibold tabular-nums">12</p>
        <p class="text-text-tertiary mt-1 text-xs">3 in progress</p>
      </div>
      <BuildingIcon size={16} class="text-text-tertiary" />
    </div>
  </Card>
</div>

TeamList.svelte

Click a card and the line below records it: onclick renders each card as a <button>.
<script lang="ts">
  import { ArrowRightIcon, Avatar, Card } from '@urbicon-ui/blocks';

  // Stand-in for your action — open the user, start a selection; the demo
  // only records who was clicked.
  let lastClicked = $state<string | null>(null);

  const users = [
    { id: '1', name: 'Anna Schulz', role: 'Administration' },
    { id: '2', name: 'Bernd Krüger', role: 'Accounting' },
    { id: '3', name: 'Cara Lentz', role: 'Caretaker' }
  ];
</script>

<div class="w-full max-w-lg">
  <div class="space-y-2">
    {#each users as user (user.id)}
      <Card variant="quiet" padding="sm" onclick={() => (lastClicked = user.name)}>
        <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>
          <ArrowRightIcon size={16} class="text-text-tertiary" />
        </div>
      </Card>
    {/each}
  </div>
  {#if lastClicked}
    <p class="text-text-tertiary mt-3 text-sm">
      Last clicked: <span class="text-text-primary font-medium">{lastClicked}</span>
    </p>
  {/if}
</div>

Where the click lives

Don't

<a href="/revenue">
  <Card variant="elevated" padding="md">
    Content
  </Card>
</a>
  • The Card keeps rendering a div: hover, focus and mint styles wait for a click source, so the card shows none of them.
  • A button or link inside the card now nests inside the outer <a> (invalid HTML, an a11y violation).

Do

<Card
  variant="elevated"
  padding="md"
  href="/revenue"
>
  Content
</Card>
  • The card itself is the <a>: one element, one tab stop, hover and focus styles included.
  • Cmd-click and middle-click open the route in a new tab, like on any link.

href for routes, onclick for actions

A destination takes href: the card renders as an <a>, and Cmd-click, middle-click and open-in-new-tab work as on any link. An action (open a dialog, pick an entry) takes onclick: the card renders as a <button type="button">. The third case is clickable: it forces the button rendering without a handler, for a wrapper component that owns the click handling itself.

No click source, no hover

Card has no decorative-hover mode: a pointer cursor and a lift on a card that goes nowhere would promise an interaction that never comes (WCAG 3.2, Predictable). Hover, the focus ring and mint switch on together with href, onclick or clickable. When a passive card seems to need the interactive look, give it its click source; wrapping it in an <a> is the pattern the comparison above warns against.

One target per card

href and onclick turn the whole card into a single <a> or <button>, and HTML allows no interactive content inside either. A row that needs a secondary action (a "Favorite" button, a menu) keeps the Card passive and puts the targets inside it: the name becomes the link, the action a Button of its own.