List

A list is a bordered surface of rows. Each row has a title, an optional description, and slots at the start and end for an avatar, an icon or a control.

Basic list

  • Routing
    src/about.ht.js becomes /about
  • Data loading
    data() runs once, at build time
  • Assets
    Only what your HTML references is bundled
list(
  listItem({ title: 'Routing', description: 'src/about.ht.js becomes /about' }),
  listItem({ title: 'Data loading', description: 'data() runs once, at build time' }),
  listItem({ title: 'Assets', description: 'Only what your HTML references is bundled' }),
)

Start and end slots

  • AL
    Ada Lovelace
    Pushed 3 commits to main
    2h
  • GH
    Grace Hopper
    Opened a pull request
    open
list(
  listItem({
    start: avatar({ name: 'Ada Lovelace', size: 'sm' }),
    title: 'Ada Lovelace',
    description: 'Pushed 3 commits to main',
    end: chip({ size: 'sm', color: 'neutral' }, '2h'),
  }),
  listItem({
    start: avatar({ name: 'Grace Hopper', size: 'sm', color: 'success' }),
    title: 'Grace Hopper',
    description: 'Opened a pull request',
    end: chip({ size: 'sm', color: 'success', dot: true }, 'open'),
  }),
)

A row with an href puts the anchor inside the <li> rather than around it, so the list stays a valid list. Do not also put a button in the row — interactive content cannot nest inside a link.

list(
  listItem({ title: 'Getting started', description: 'Install and first page', href: '/docs' }),
  listItem({ title: 'Routing', description: 'File-based, with dynamic segments', href: '/docs/routing' }),
  listItem({ title: 'Deployment', description: 'Netlify, Vercel, Pages, Amplify', href: '/docs/deployment' }),
)

Rows with controls

When a row holds a switch or a button, leave the row itself unlinked and let the control be the interactive part.

  • Pagefind search
    Indexes every page at the end of the build
  • Image optimization
    Resizes and converts images. Needs sharp.
  • Server islands
    Renders marked regions at request time
list(
  listItem({
    title: 'Pagefind search',
    description: 'Indexes every page at the end of the build',
    end: toggle({ 'aria-label': 'Pagefind search', checked: true }),
  }),
  listItem({
    title: 'Image optimization',
    description: 'Resizes and converts images. Needs sharp.',
    end: toggle({ 'aria-label': 'Image optimization', checked: true }),
  }),
  listItem({
    title: 'Server islands',
    description: 'Renders marked regions at request time',
    end: toggle({ 'aria-label': 'Server islands' }),
  }),
)

Plain

plain drops the border and background, for a list that sits inside a card or a sidebar that already has its own surface.

Recent builds

  • 94a837a
    main · 4 minutes ago
    passed
  • dcfaaae
    main · 2 hours ago
    passed
  • a46a461
    main · yesterday
    failed
card(
  cardHeader({ title: 'Recent builds' }),
  cardBody(
    list({ plain: true },
      listItem({ title: '94a837a', description: 'main · 4 minutes ago', end: chip({ size: 'sm', color: 'success', dot: true }, 'passed') }),
      listItem({ title: 'dcfaaae', description: 'main · 2 hours ago', end: chip({ size: 'sm', color: 'success', dot: true }, 'passed') }),
      listItem({ title: 'a46a461', description: 'main · yesterday', end: chip({ size: 'sm', color: 'danger', dot: true }, 'failed') }),
    ),
  ),
)

Free-form rows

Without title or description, a row renders whatever children it is given — for a layout the two-line shape does not cover.

  • Custom row

    Anything you like inside
list(
  listItem(
    stack({ direction: 'row', gap: 'md', align: 'center', justify: 'space-between', style: 'width: 100%' },
      stack({ gap: 'none' },
        text({ variant: 'small' }, 'Custom row'),
        text({ variant: 'caption', tone: 'muted' }, 'Anything you like inside'),
      ),
      button({ size: 'sm', variant: 'soft' }, 'Action'),
    ),
  ),
)

From data

  • E
    English
    24 pages
    en
  • E
    Español
    24 pages
    es
  • Z
    简体中文
    24 pages
    zh
return (() => {
  const locales = [
    { code: 'en', name: 'English', pages: 24 },
    { code: 'es', name: 'Español', pages: 24 },
    { code: 'zh', name: '简体中文', pages: 24 },
  ]

  return list(
    locales.map((locale) =>
      listItem({
        start: avatar({ name: locale.code, size: 'sm', color: 'neutral', square: true }),
        title: locale.name,
        description: locale.pages + ' pages',
        end: chip({ size: 'sm', color: 'neutral' }, locale.code),
      }),
    ),
  )
})()

Props

list():

PropTypeDefaultDescription
plainbooleanfalseDrop the border and background.
asstring'ul'Element to render, e.g. ol.

listItem():

PropTypeDefaultDescription
titleChildThe row’s main line.
descriptionChildA muted second line.
startChildLeading slot — an avatar or icon.
endChildTrailing slot — a chip, a control, a timestamp.
hrefstringMakes the row a link, with the anchor inside the li.
interactivebooleanfalseHover highlight without making it a link.