# Card

> A container for grouping related content with optional header and footer

Status: stable. Added in Components 2.0.0. Updated in Components 2.17.0, 24 Oct 2026.

A container for grouping related content with optional header and footer.

## Import

```tsx
import { Card } from '@oztix/roadie-components/card'
```

## Examples

### Default

```tsx
<Card href='#'>
  <Card.Header>
    <Card.Title>Midnight Frequency</Card.Title>
    <Card.Description>Live at Clockwork Wattle Brewery, Brisbane</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>An evening of electronic beats and ambient soundscapes under the stars.</p>
  </Card.Content>
</Card>
```

### Variants

`variant` sets the card's shape. `default` is a plain card and applies when you leave the prop off. `ticket` cuts a notch into each side where the body meets `Card.Footer`, with a perforated line between them. The notches are real holes, so whatever sits behind the card shows through. `ticket` works with every emphasis. On `subtler`, the fill clears when the card is interactive, the same as a plain subtler card.

```tsx
<div className='grid max-w-sm gap-6'>
  <div className='grid gap-2'>
    <p className='text-sm text-subtle'>default</p>
    <Card emphasis='raised'>
      <Card.Header>
        <Card.Title>Marzipan Thunderclap</Card.Title>
        <Card.Description>Fri 12 Mar · Nimbus Thistle Hall, Newcastle</Card.Description>
      </Card.Header>
      <Card.Content>
        <p>Doors open at 7pm. Bring your ticket on your phone.</p>
      </Card.Content>
    </Card>
  </div>
  <div className='grid gap-2'>
    <p className='text-sm text-subtle'>ticket</p>
    <div className='grid rounded-2xl bg-subtle p-4 intent-brand'>
      <Card variant='ticket' emphasis='raised' intent='neutral'>
        <Card.Content className='grid justify-items-center gap-4 p-6 pb-5'>
          <QRCode value='A7K2MKWX' aria-label='Ticket QR code for General admission' />
          <p className='text-display-ui-4 text-strong'>General admission</p>
        </Card.Content>
        <Card.Footer className='grid gap-4'>
          <div className='flex items-center gap-3'>
            <IconTile emphasis='subtler' size='lg'>
              <UserCircleIcon weight='bold' />
            </IconTile>
            <div className='grid min-w-0 flex-1'>
              <p className='font-semibold text-strong'>Sam Okafor</p>
              <p className='truncate text-sm text-subtle'>sam.okafor@example.com</p>
            </div>
            <IconButton aria-label='Edit ticket holder' emphasis='subtle'>
              <PencilSimpleIcon weight='bold' />
            </IconButton>
          </div>
          <div className='grid grid-cols-[1fr_auto] gap-2'>
            <Button emphasis='subtle' className='w-full'>
              <WalletIcon weight='bold' />
              Add to Wallet
            </Button>
            <Button emphasis='subtle'>
              <ExportIcon weight='bold' />
              Share
            </Button>
          </div>
        </Card.Footer>
      </Card>
    </div>
  </div>
</div>
```

### Direction

`direction` lays out the card's parts and works on any card. `vertical` stacks them and applies when you leave the prop off. `horizontal` gives `Card.Footer` a column of its own beside the rest of the card, sized to its contents. `auto` picks between the two: the card stacks in a narrow container and splits at 30rem and up, laying out exactly as `horizontal` does once it splits. An `auto` card is a container named `card`, so its children can use `@min-[30rem]/card:` and resolve against the card rather than a container further up.

Direction moves the footer, it does not change what belongs in each part. Keep the body in `Card.Content` and leave the footer for the stub, such as actions or counts.

The footer's column is as wide as its content, so cards in a list can have columns of different widths. Set `--card-footer-size` once on the list to line them up. Every side column is then at least that wide, and a ticket's perforation follows it. A stacked footer ignores it.

```tsx
<div className='grid gap-4 [--card-footer-size:11rem]'>
  {events.map((event) => (
    <Card key={event.id} variant='ticket' direction='auto'>…</Card>
  ))}
</div>
```

```tsx
{/* vertical */}
<div className='grid rounded-2xl bg-subtle p-4 intent-brand'>
  <Card variant='ticket' direction='vertical' emphasis='raised' intent='neutral'>
    <Card.Header className='flex items-center gap-4'>
      <Card.Image
        src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
        alt='Corduroy Lagoon Sessions'
        className='aspect-square w-14 object-cover'
        width={112}
        height={112}
      />
      <div className='grid gap-0.5'>
        <Card.Title>Corduroy Lagoon Sessions</Card.Title>
        <Card.Description>Sat 18 Oct · 7pm · Gooseberry Signal Warehouse, Marrickville</Card.Description>
      </div>
    </Card.Header>
    <Card.Content>
      <p>Doors open at 6.30pm.</p>
    </Card.Content>
    <Card.Footer>
      <Button emphasis='subtle' className='w-full'>
        <TicketIcon weight='bold' />
        28 tickets
      </Button>
      <Button emphasis='subtle' className='w-full'>
        <UsersIcon weight='bold' />
        7 shared
      </Button>
    </Card.Footer>
  </Card>
</div>
{/* horizontal */}
<div className='grid rounded-2xl bg-subtle p-4 intent-brand'>
  <Card variant='ticket' direction='horizontal' emphasis='raised' intent='neutral'>
    <Card.Header className='flex items-center gap-4'>
      <Card.Image
        src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
        alt='Corduroy Lagoon Sessions'
        className='aspect-square w-14 object-cover'
        width={112}
        height={112}
      />
      <div className='grid gap-0.5'>
        <Card.Title>Corduroy Lagoon Sessions</Card.Title>
        <Card.Description>Sat 18 Oct · 7pm · Gooseberry Signal Warehouse, Marrickville</Card.Description>
      </div>
    </Card.Header>
    <Card.Content>
      <p>Doors open at 6.30pm.</p>
    </Card.Content>
    <Card.Footer>
      <Button emphasis='subtle' className='w-full'>
        <TicketIcon weight='bold' />
        28 tickets
      </Button>
      <Button emphasis='subtle' className='w-full'>
        <UsersIcon weight='bold' />
        7 shared
      </Button>
    </Card.Footer>
  </Card>
</div>
{/* auto, narrow container */}
<div className='grid max-w-xs rounded-2xl bg-subtle p-4 intent-brand'>
  <Card variant='ticket' direction='auto' emphasis='raised' intent='neutral'>
    <Card.Header className='flex items-center gap-4'>
      <Card.Image
        src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
        alt='Corduroy Lagoon Sessions'
        className='aspect-square w-14 object-cover'
        width={112}
        height={112}
      />
      <div className='grid gap-0.5'>
        <Card.Title>Corduroy Lagoon Sessions</Card.Title>
        <Card.Description>Sat 18 Oct · 7pm · Gooseberry Signal Warehouse, Marrickville</Card.Description>
      </div>
    </Card.Header>
    <Card.Content>
      <p>Doors open at 6.30pm.</p>
    </Card.Content>
    <Card.Footer>
      <Button emphasis='subtle' className='w-full'>
        <TicketIcon weight='bold' />
        28 tickets
      </Button>
      <Button emphasis='subtle' className='w-full'>
        <UsersIcon weight='bold' />
        7 shared
      </Button>
    </Card.Footer>
  </Card>
</div>
{/* auto, room to split */}
<div className='grid rounded-2xl bg-subtle p-4 intent-brand'>
  <Card variant='ticket' direction='auto' emphasis='raised' intent='neutral'>
    <Card.Header className='flex items-center gap-4'>
      <Card.Image
        src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
        alt='Corduroy Lagoon Sessions'
        className='aspect-square w-14 object-cover'
        width={112}
        height={112}
      />
      <div className='grid gap-0.5'>
        <Card.Title>Corduroy Lagoon Sessions</Card.Title>
        <Card.Description>Sat 18 Oct · 7pm · Gooseberry Signal Warehouse, Marrickville</Card.Description>
      </div>
    </Card.Header>
    <Card.Content>
      <p>Doors open at 6.30pm.</p>
    </Card.Content>
    <Card.Footer>
      <Button emphasis='subtle' className='w-full'>
        <TicketIcon weight='bold' />
        28 tickets
      </Button>
      <Button emphasis='subtle' className='w-full'>
        <UsersIcon weight='bold' />
        7 shared
      </Button>
    </Card.Footer>
  </Card>
</div>
```

### Emphasis

```tsx
<Card href='#' emphasis='raised'>
  <Card.Header>
    <Card.Title>Raised</Card.Title>
    <Card.Description>Featured event</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Use for primary or featured content that should stand out.</p>
  </Card.Content>
</Card>
<Card href='#' emphasis='normal'>
  <Card.Header>
    <Card.Title>Normal</Card.Title>
    <Card.Description>Standard event</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Default emphasis for general-purpose cards.</p>
  </Card.Content>
</Card>
<Card href='#' emphasis='subtle'>
  <Card.Header>
    <Card.Title>Subtle</Card.Title>
    <Card.Description>Related event</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Use for secondary or supporting content.</p>
  </Card.Content>
</Card>
<Card href='#' emphasis='subtler'>
  <Card.Header>
    <Card.Title>Subtler</Card.Title>
    <Card.Description>Background content</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Invisible at rest. It reveals on hover. Use for interactive lists or grids.</p>
  </Card.Content>
</Card>
```

### Intents

```tsx
<Card href='#' intent='accent' emphasis='subtle'>
  <Card.Content>
    <p className='text-sm font-semibold'>On sale now</p>
    <p className='text-sm'>Early bird tickets available until Friday.</p>
  </Card.Content>
</Card>
<Card href='#' intent='success' emphasis='subtle'>
  <Card.Content>
    <p className='text-sm font-semibold'>Booking confirmed</p>
    <p className='text-sm'>Your tickets for Quokka Thunder Weekender have been reserved.</p>
  </Card.Content>
</Card>
<Card href='#' intent='danger' emphasis='subtle'>
  <Card.Content>
    <p className='text-sm font-semibold'>Almost sold out</p>
    <p className='text-sm'>Only 12 tickets remaining for Sapphire Cassowary Night.</p>
  </Card.Content>
</Card>
```

### With image

The subtler card has padding around the image, giving it inset rounded corners. The normal card uses a full-bleed image with `overflow-hidden`.

`Card.Image` is an [`Image`](https://ticketsolutionsptyltd.github.io/roadie/components/image.md). Pass `width` (and `height`) to serve a right-sized WebP instead of the original, `sizes` so the fluid card loads small files on small screens, and `placeholder='blur'` for a blur-up while it loads.

```tsx
<div className='grid grid-cols-2 gap-4'>
  <Card href='#' emphasis='subtler'>
    <Card.Image
      src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
      alt='Yabby Lightning Tour'
      className='aspect-square'
      width={300}
      height={300}
      sizes='(min-width: 640px) 25vw, 50vw'
      placeholder='blur'
    />
    <Card.Content>
      <Card.Title>Yabby Lightning Tour</Card.Title>
      <Card.Description>
        <DateTime
          at={new Date('2026-05-17T08:00:00Z')}
          timeZone='Australia/Brisbane'
          timeStyle='medium'
        />
      </Card.Description>
      <Card.Description>Lighthouse Fig Lawn</Card.Description>
    </Card.Content>
    <Card.Footer>
      <p className='text-strong font-semibold'>$89</p>
    </Card.Footer>
  </Card>
  <Card href='#' className='overflow-hidden'>
    <Card.Image
      src='https://assets.oztix.com.au/image/1226ab55-3d53-47f0-ab4c-cddcf02bb001.png'
      alt='Windmill Pear Long Table Dinner'
      width={600}
      height={300}
      sizes='(min-width: 640px) 25vw, 50vw'
    />
    <Card.Content>
      <Card.Description>Dining &amp; Social</Card.Description>
      <Card.Title>Windmill Pear Long Table Dinner</Card.Title>
      <Card.Description>
        <DateTime at={new Date('2026-05-24T08:00:00Z')} timeZone='Australia/Brisbane' />{' '}
        &middot; <Duration of='PT3H' durationStyle='medium' />
      </Card.Description>
    </Card.Content>
  </Card>
</div>
```

### Composition

Every card has a `Card.Content`, which holds its body. `Card.Header` above it and `Card.Footer` below it are optional. A header has no bottom padding, so a card with only a title and description puts them in `Card.Content`, as the [main link](#with-a-main-link) example does. A card without `Card.Content` warns in development. Keep a `Suspense` boundary inside `Card.Content`, not around it, or the card warns while it loads.

```tsx
<Card>
  <Card.Header>
    <Card.Title>Quokka Thunder Weekender</Card.Title>
    <Card.Description>3-day music festival, Jumbuck Orchard Park</Card.Description>
  </Card.Header>
  <Card.Content>
    <p>Three days of live music across four stages featuring local and international artists. Food trucks, art installations, and late-night DJ sets.</p>
  </Card.Content>
  <Card.Footer>
    <Button size='sm'>Buy tickets</Button>
    <Button size='sm' emphasis='subtler'>Learn more</Button>
  </Card.Footer>
</Card>
```

### With a main link

`href` on `Card` makes the whole card one link, so nothing else inside it can be a link or a button. When a card also holds other actions, wrap its title in `Card.Link` instead. The link covers the card, so a click anywhere follows it, and the other links and buttons sit above it and stay clickable. The card takes its hover, press and focus states from the link, and screen readers hear a short link named by the title rather than the whole card.

```tsx
<Card variant='ticket' direction='auto' emphasis='raised' className='max-w-lg'>
  <Card.Content>
    <Card.Title>
      <Card.Link href='#weekly-pass'>Weekly Pass</Card.Link>
    </Card.Title>
    <Card.Description>Mon 3 to Fri 7 May · Barnacle Bowl Amphitheatre, Rockhampton</Card.Description>
  </Card.Content>
  <Card.Footer>
    <Button size='sm' href='#tickets'>
      <TicketIcon weight='bold' className='size-4' />
      638 tickets
    </Button>
    <Button size='sm' href='#shared'>
      <UsersIcon weight='bold' className='size-4' />
      376 shared
    </Button>
  </Card.Footer>
</Card>
```

## API reference

### Card

```tsx
import { Card } from '@oztix/roadie-components/card'
```

- `as`: `ElementType`. Pass `render={<button onClick=… />}` for a clickable card, or `render={<MyLink href='/x' />}` for a custom link wrapper. The `render` prop accepts an element, a component, or a function `(defaultProps) => element` for full control. Deprecated: Use `render` instead. `as` will be removed in v3.0.0.
- `href`: `string`. Pass an href to render the card as a routed anchor. Internal hrefs route through the configured `RoadieLinkProvider`; external hrefs (`http(s)://`, `//…`) render `<a target='_blank' rel='noopener noreferrer'>`; `mailto:` / `tel:` / `sms:` render plain `<a>`. Use `external` to override the auto classification. `render` always wins over `href` smart-routing — pass `render` to render a non-anchor or to bypass provider routing.
- `external`: `boolean`. Force external-link treatment when `href` is set. Has no effect when `render` is set — the consumer's element is responsible for its own target/rel.
- `target`: `string`. Override the auto `target='_blank'` default on external hrefs.
- `rel`: `string`. Override the auto `rel='noopener noreferrer'` default on external hrefs.
- `render`: `RoadieRenderProp`. Escape hatch — swap the underlying element with full control over the rendered shape. See [Linking](https://ticketsolutionsptyltd.github.io/roadie/foundations/linking.md) for the full reference.
- `variant`: `"default" | "ticket"`. Defaults to `'default'`. The card's shape. `default` is a plain card. `ticket` cuts a notch into each side where the body meets `Card.Footer`, with a perforated line between them; it works with every emphasis.
- `direction`: `"auto" | "horizontal" | "vertical"`. Defaults to `'vertical'`. Layout flow. `vertical` stacks every part. `horizontal` gives `Card.Footer` a column of its own beside the rest of the card. `auto` stacks below 30rem and splits into two columns at or above it. On a `ticket`, the notches and perforation follow the layout in effect.
- `intent`: `"neutral" | "brand" | "brand-secondary" | "brand-blue" | "brand-orange" | "brand-purple" | "accent" | "danger" | "success" | "warning" | "info" | null`.
- `emphasis`: `"raised" | "subtle" | "subtler" | "normal" | null`.

### Card.Content

The card's body, which every card needs. A card with only a title and description puts them here, since `Card.Header` has no bottom padding. Without one, Card warns in development.

No props beyond the standard HTML attributes.

### Card.Description

No props beyond the standard HTML attributes.

### Card.Footer

No props beyond the standard HTML attributes.

### Card.Header

An optional top part, above `Card.Content`. It has no bottom padding, so a card never ends with it.

No props beyond the standard HTML attributes.

### Card.Image

- `src`: `string`. Required. Image URL. Oztix-CDN hosts get the resize/transcode rewrite; everything else passes through.
- `alt`: `string`. Required. Alt text describing the image. Required.
- `width`: `number`. 1x render width in CSS pixels. Drives the default `srcSet`/`sizes` and reserves layout. Omit to render a plain pass-through `<img>`.
- `height`: `number`. Target/intrinsic height. Reserves layout (via `aspect-ratio`) and, when set alongside `width`, is sent to the proxy so it crops to both dimensions.
- `widths`: `number[]`. Explicit srcSet ladder. Defaults to `[width, width * 2]`.
- `sizes`: `string`. `<img sizes>` describing how wide the image renders (e.g. `'100vw'`). Set it for fluid images so Roadie builds a responsive ladder and small screens load small files. Defaults to `${width}px` (fixed size).
- `priority`: `boolean`. Marks the image as LCP-critical: eager loading + `fetchpriority="high"`.
- `format`: `"webp" | "png" | "jpg"`. Output format. Defaults to `webp`.
- `quality`: `number`. Encoder quality, 1–100. Lower trades fidelity for bytes.
- `autotrim`: `boolean`. Crop surrounding transparent space server-side (ImageSharp `autotrim=1`). Useful for logos padded with transparency. Takes effect with `width`.
- `params`: `Record<string, string | number | boolean>`. Escape hatch for any other ImageSharp.Web command (`rmode`, `ranchor`, `bgcolor`, etc.), merged verbatim. Takes effect with `width`.
- `placeholder`: `"blur" | "empty"`. Blur-up placeholder: show a tiny low-quality preview, then fade the full image in on load. `'blur'` wraps the image in a positioned element. The preview is derived from the Oztix proxy by default; pass `blurDataURL` for non-Oztix images or to override.
- `blurDataURL`: `string`. Explicit preview source for `placeholder='blur'`. Required for non-Oztix images.
- `sources`: `ImageSource[]`. Art direction: render a `<picture>` with a `<source>` per breakpoint, each able to use a different URL/crop. The component `src` stays the fallback `<img>`. Each entry inherits `format`/`quality`/`autotrim`/`params`/`width` unless it overrides them.
- `defer`: `boolean`. Defer loading until the image nears the viewport via IntersectionObserver. Use inside horizontally-scrolled surfaces (e.g. Carousel) where native `loading='lazy'` over-fetches. Ignored when `priority` is set.

### Card.Link

The card's main link, usually wrapping the title text. It covers the whole card, and other links and buttons inside the card stay clickable above it.

- `href`: `string`. Required. Where the card goes. Internal hrefs route through the configured `RoadieLinkProvider`; external hrefs open in a new tab.
- `external`: `boolean`. Force external-link treatment.

### Card.Title

No props beyond the standard HTML attributes.

Docs updated 20 Oct 2026.
