Card

A card groups related content on its own surface. The parts — cardHeader(), cardMedia(), cardBody(), cardFooter() — are separate functions rather than props, so you use only the ones you need and put them in whatever order the design wants.

Basic card

File-based routing

src/about.ht.js → /about

Folders become paths. Brackets become parameters. There is no router to configure.

card(
  cardHeader({ title: 'File-based routing', subtitle: 'src/about.ht.js → /about' }),
  cardBody(text({ variant: 'small', tone: 'muted' }, 'Folders become paths. Brackets become parameters. There is no router to configure.')),
)

Variants

Outlined is the default. Elevated trades the border for a shadow, and flat tints the surface instead of either.

Outlined

Elevated

Flat

grid({ min: '13rem' },
  card({ variant: 'outlined' }, cardBody(text({ variant: 'small' }, 'Outlined'))),
  card({ variant: 'elevated' }, cardBody(text({ variant: 'small' }, 'Elevated'))),
  card({ variant: 'flat' }, cardBody(text({ variant: 'small' }, 'Flat'))),
)

divided adds the hairline above the footer. The footer is pushed to the bottom, so cards in a row line their actions up even when the text above is different lengths.

Basic site

A minimal project plus deploy configs.

Markdown blog

A folder of .md files rendered to static pages, with an RSS feed and no client JavaScript at all.

grid({ min: '14rem' },
  card(
    cardHeader({ title: 'Basic site' }),
    cardBody(text({ variant: 'small', tone: 'muted' }, 'A minimal project plus deploy configs.')),
    cardFooter({ divided: true }, button({ size: 'sm', variant: 'soft' }, 'Open')),
  ),
  card(
    cardHeader({ title: 'Markdown blog' }),
    cardBody(text({ variant: 'small', tone: 'muted' }, 'A folder of .md files rendered to static pages, with an RSS feed and no client JavaScript at all.')),
    cardFooter({ divided: true }, button({ size: 'sm', variant: 'soft' }, 'Open')),
  ),
)

Media

cardMedia() fills the top of the card at a fixed aspect ratio, so a row of cards stays even whatever the source images measure.

Default 16 / 9

ratio: 4 / 3

grid({ min: '13rem' },
  card(
    cardMedia({ src: '/logo.svg', alt: '', style: 'background: var(--su-surface-2); object-fit: contain; padding: 1rem' }),
    cardBody(text({ variant: 'small' }, 'Default 16 / 9')),
  ),
  card(
    cardMedia({ src: '/logo.svg', alt: '', ratio: '4 / 3', style: 'background: var(--su-surface-2); object-fit: contain; padding: 1rem' }),
    cardBody(text({ variant: 'small' }, 'ratio: 4 / 3')),
  ),
)

Give the card an href and the whole surface becomes one link, with the hover lift that goes with it. Do not put buttons or other links inside a card in this form — interactive content cannot nest inside a link. Use a footer button on a plain card instead.

grid({ min: '14rem' },
  card({ href: '/docs/routing' },
    cardHeader({ title: 'Routing', subtitle: 'Read the guide' }),
    cardBody(text({ variant: 'small', tone: 'muted' }, 'Dynamic routes, catch-alls and route groups.')),
  ),
  card({ href: '/docs/data' },
    cardHeader({ title: 'Data loading', subtitle: 'Read the guide' }),
    cardBody(text({ variant: 'small', tone: 'muted' }, 'data() runs at build time, with fetch caching.')),
  ),
)

Padding

One prop sets the padding for every part of the card at once.

padding: sm

padding: xl

stack({ gap: 'md' },
  card({ padding: 'sm' }, cardBody(text({ variant: 'small' }, 'padding: sm'))),
  card({ padding: 'xl' }, cardBody(text({ variant: 'small' }, 'padding: xl'))),
)

Free-form contents

The parts are a convenience, not a requirement — a card takes any children, and cardHeader() accepts children of its own alongside the title, for an avatar or a menu button on the right.

Paul Browne

Deployed 4 minutes ago

PB
Build passed12 pages4.1 kB
card(
  cardHeader(
    { title: 'Paul Browne', subtitle: 'Deployed 4 minutes ago' },
    avatar({ name: 'Paul Browne', size: 'sm' }),
  ),
  cardBody(
    stack({ direction: 'row', gap: 'sm', wrap: true },
      chip({ color: 'success', dot: true }, 'Build passed'),
      chip({ color: 'neutral' }, '12 pages'),
      chip({ color: 'neutral' }, '4.1 kB'),
    ),
  ),
)

Props

card():

PropTypeDefaultDescription
variant'outlined' | 'elevated' | 'flat''outlined'How the surface is separated from the page.
hrefstringRenders the whole card as a link.
paddingSpace'lg'Padding used by every part of the card.

The parts:

PartPropsDefaultDescription
cardHeadertitle, subtitleTitle and subtitle, plus any children alongside them.
cardTitleas'h3'The title on its own, when the header is hand-built.
cardSubtitleThe muted line under a title.
cardMediasrc, alt, ratio'16 / 9'A cover image at a fixed aspect ratio.
cardBodyThe main content region.
cardFooterdividedfalseBottom row of actions; divided adds the hairline above it.