Docs

This page isn't translated yet

List

Bullet, number and check lists, and CustomList with markers.

On this page

List and ListItem render bullet, numbered and check lists whose markers, spacing and colors come from the list tokens. CustomList lays out items with a marker of your choice, such as an icon or an emoji, in a two-column grid.

List
  • Tokens exported from Figma as DTCG JSON
  • CSS variables and Tailwind utilities generated on sync
  • Typed React and Next.js components that read them
import { List, ListItem } from '@systhemaui/next'

export default function Demo() {
  return (
    <List type="bullet">
      <ListItem>Tokens exported from Figma as DTCG JSON</ListItem>
      <ListItem>CSS variables and Tailwind utilities generated on sync</ListItem>
      <ListItem>Typed React and Next.js components that read them</ListItem>
    </List>
  )
}

ImportLink to this section

import {
  CustomList,
  CustomListContent,
  CustomListItem,
  CustomListItemMarker,
  List,
  ListItem,
} from '@systhemaui/next'

In a React app without Next.js, import them from @systhemaui/react. The API is the same.

Variants and tagsLink to this section

The type prop picks the marker. bullet (the default) and check render a <ul>, number renders an <ol>. Use as to choose another element.

List types

bullet

  • Header
  • Hero
  • Footer

number

  1. Install
  2. Configure
  3. Sync

check

  • Checked Tokens
  • Checked Components
  • Unchecked Content
import { List, ListItem, Paragraph } from '@systhemaui/next'

export default function Demo() {
  return (
    <div className="grid w-full gap-8 sm:grid-cols-3">
      <div>
        <Paragraph type="label">bullet</Paragraph>
        <List type="bullet">
          <ListItem>Header</ListItem>
          <ListItem>Hero</ListItem>
          <ListItem>Footer</ListItem>
        </List>
      </div>
      <div>
        <Paragraph type="label">number</Paragraph>
        <List type="number">
          <ListItem>Install</ListItem>
          <ListItem>Configure</ListItem>
          <ListItem>Sync</ListItem>
        </List>
      </div>
      <div>
        <Paragraph type="label">check</Paragraph>
        <List type="check">
          <ListItem checked>Tokens</ListItem>
          <ListItem checked>Components</ListItem>
          <ListItem checked={false}>Content</ListItem>
        </List>
      </div>
    </div>
  )
}

With the default tokens, bullets are en dashes, numbers are followed by a period, and check items show a check mark or a cross.

ExamplesLink to this section

Check listsLink to this section

In a check list, checked decides the marker: true shows the check mark, false (or no value) the cross. When checked is set, the item starts with visually hidden "Checked" or "Unchecked" text, so screen readers announce the state. Change the words with checkedLabel and uncheckedLabel, for example to translate them.

Check list
  • Inbegriffen Unbegrenzte Seiten
  • Inbegriffen Eigene Domain
  • Nicht inbegriffen Mehrsprachige Inhalte
import { List, ListItem } from '@systhemaui/next'

export default function Demo() {
  return (
    <List type="check">
      <ListItem checked checkedLabel="Inbegriffen">
        Unbegrenzte Seiten
      </ListItem>
      <ListItem checked checkedLabel="Inbegriffen">
        Eigene Domain
      </ListItem>
      <ListItem checked={false} uncheckedLabel="Nicht inbegriffen">
        Mehrsprachige Inhalte
      </ListItem>
    </List>
  )
}

Change the two icons with blocks.list.checkedIcon and blocks.list.uncheckedIcon (see Component CSS blocks).

Nested listsLink to this section

Put a nested list in its own ListItem, the way rich text from the Lexical editor arrives. That item shows no marker and does not count in a numbered list, so the numbering continues after it. Mark the inner items nested so they do not run their own scroll reveal inside the parent's.

Nested list
  1. Install the packages
    • @systhemaui/core
    • @systhemaui/next
  2. Run the first sync
  3. Start the dev server
import { List, ListItem } from '@systhemaui/next'

export default function Demo() {
  return (
    <List type="number">
      <ListItem>Install the packages</ListItem>
      <ListItem>
        <List type="bullet">
          <ListItem nested>@systhemaui/core</ListItem>
          <ListItem nested>@systhemaui/next</ListItem>
        </List>
      </ListItem>
      <ListItem>Run the first sync</ListItem>
      <ListItem>Start the dev server</ListItem>
    </List>
  )
}

Custom markersLink to this section

CustomList is a two-column grid: CustomListItemMarker in the first column, CustomListContent in the second. The marker is sized from --list-marker-size and takes the list marker color, so an emoji, a character or an icon all line up.

Custom list
  • Fluid spacing that scales with the viewport
  • Two color systems, switchable per section
  • Payload blocks that render the same components
import {
  CustomList,
  CustomListContent,
  CustomListItem,
  CustomListItemMarker,
} from '@systhemaui/next'

const features = [
  { marker: '⚡', text: 'Fluid spacing that scales with the viewport' },
  { marker: '🎨', text: 'Two color systems, switchable per section' },
  { marker: '🧩', text: 'Payload blocks that render the same components' },
]

export default function Demo() {
  return (
    <CustomList>
      {features.map((feature) => (
        <CustomListItem key={feature.text}>
          <CustomListItemMarker aria-hidden="true">{feature.marker}</CustomListItemMarker>
          <CustomListContent>{feature.text}</CustomListContent>
        </CustomListItem>
      ))}
    </CustomList>
  )
}

For an SVG marker, give the <svg> a size of 1em; the marker sets its font size to --list-icon-size.

Icon markers
  • Free updates for a year
  • Priority support
  • Unlimited projects
import {
  CustomList,
  CustomListContent,
  CustomListItem,
  CustomListItemMarker,
} from '@systhemaui/next'

export default function Demo() {
  return (
    <CustomList>
      {['Free updates for a year', 'Priority support', 'Unlimited projects'].map((text) => (
        <CustomListItem key={text}>
          <CustomListItemMarker>
            <svg
              width="1em"
              height="1em"
              viewBox="0 -960 960 960"
              fill="currentColor"
              aria-hidden="true"
              focusable="false"
            >
              <path d="m233-120 65-281L80-590l288-25 112-265 112 265 288 25-218 189 65 281-247-149-247 149Z" />
            </svg>
          </CustomListItemMarker>
          <CustomListContent>{text}</CustomListContent>
        </CustomListItem>
      ))}
    </CustomList>
  )
}

PropsLink to this section

ListLink to this section

List takes the props of the element it renders (<ul> or <ol>, or the as element).

PropTypeDefaultDescription
asElementType'ul'
type'bullet' | 'number' | 'check'-
classNamestring-
…and all <ul> attributes

ListItemLink to this section

checked is only meaningful in a check list. checkedLabel defaults to 'Checked' and uncheckedLabel to 'Unchecked'; they are rendered only when checked is set. nested and disableAnimation both drop the configured animationClasses.

PropTypeDefaultDescription
asElementType'li'
nestedboolean-
classNamestring-
disableAnimationboolean-
checkedboolean-
checkedLabelstring-
uncheckedLabelstring-
…and all <li> attributes

Custom list partsLink to this section

CustomList renders a <ul> and takes disableAnimation. CustomListItem renders an <li>, CustomListItemMarker and CustomListContent render <span>s; each takes as for another element.

PropTypeDefaultDescription
asElementType'ul'
classNamestring-
disableAnimationboolean-
…and all <ul> attributes
PropTypeDefaultDescription
asElementType'li'
classNamestring-
…and all <li> attributes
PropTypeDefaultDescription
asElementType'span'
classNamestring-
…and all <span> attributes
PropTypeDefaultDescription
asElementType'span'
classNamestring-
…and all <span> attributes

HTML and CSSLink to this section

Without React, put a list class on the <ul> or <ol>. Markers are ::before pseudo-elements, so the items need no extra markup; add is-checked to a checked item in a check list:

<ul class="list-bullet text-body color-body">
  <li>Header</li>
  <li>Footer</li>
</ul>

<ol class="list-number text-body color-body">
  <li>Install</li>
  <li>Sync</li>
</ol>

<ul class="list-check text-body color-body">
  <li class="is-checked"><span class="sr-only!">Checked </span>Tokens</li>
  <li><span class="sr-only!">Unchecked </span>Content</li>
</ul>

<ul class="list-custom text-body color-body">
  <li class="list-custom-entry">
    <span class="list-custom-entry-marker" aria-hidden="true">⚡</span>
    <span class="list-entry-custom-content">Fluid spacing</span>
  </li>
</ul>

The marker size, gaps and color come from the list tokens (--list-marker-size, --list-icon-size, --list-space-x, --list-gap-y, --color-list-marker). The list classes also suppress the native marker, so a project rule that sets list-style-type cannot show two markers.

ClassStyles
list-bullet:is(.list-bullet > li, .list-number > li, .list-check > li) { list-style: none; position: relative; padding-left: calc(var(--list-space-x) + var(--list-marker-size)); margin-block: var(--list-gap-y); }:is(.list-bullet > li :first-child, .list-number > li :first-child, .list-check > li :first-child) { margin-top: 0; }:is(.list-bullet > li :last-child, .list-number > li :last-child, .list-check > li :last-child) { margin-bottom: 0; }+ 4 more rules
list-number:is(.list-bullet > li, .list-number > li, .list-check > li) { list-style: none; position: relative; padding-left: calc(var(--list-space-x) + var(--list-marker-size)); margin-block: var(--list-gap-y); }:is(.list-bullet > li :first-child, .list-number > li :first-child, .list-check > li :first-child) { margin-top: 0; }:is(.list-bullet > li :last-child, .list-number > li :last-child, .list-check > li :last-child) { margin-bottom: 0; }+ 7 more rules
list-check:is(.list-bullet > li, .list-number > li, .list-check > li) { list-style: none; position: relative; padding-left: calc(var(--list-space-x) + var(--list-marker-size)); margin-block: var(--list-gap-y); }:is(.list-bullet > li :first-child, .list-number > li :first-child, .list-check > li :first-child) { margin-top: 0; }:is(.list-bullet > li :last-child, .list-number > li :last-child, .list-check > li :last-child) { margin-bottom: 0; }+ 7 more rules
is-checked:is(.list-check > .is-checked)::before { mask-image: url("data:image/svg+xml,…"); }
list-custom& { display: grid; align-items: start; grid-template-columns: auto 1fr; column-gap: var(--list-space-x); row-gap: var(--list-gap-y); }:is(.list-custom > li:has( > :is(ul, ol))):before { --tw-content: none; }
list-custom-entrydisplay: contents;
list-custom-entry-markergrid-column-start: 1;width: var(--list-marker-size);height: var(--list-marker-size);display: inline-grid;place-items: center;font-size: var(--list-icon-size);line-height: 1;color: var(--color-list-marker);
list-entry-custom-marker:is(.list-entry-custom-marker > *) { width: 100%; height: 100%; }:is(.list-entry-custom-marker > img, .list-entry-custom-marker > svg) { object-fit: contain; object-position: center; }
list-entry-custom-contentgrid-column-start: 2;

Next.jsLink to this section

@systhemaui/next re-exports the list components from @systhemaui/react unchanged. They are server-compatible.

AccessibilityLink to this section

  • Bullet and number markers are CSS, and the elements are real <ul>, <ol> and <li>, so screen readers announce the list and its length.
  • A check mark is a picture, not text. Set checked on every item of a check list so the state is announced, and translate checkedLabel and uncheckedLabel with the page.
  • Hide a decorative custom marker with aria-hidden="true".

See Lists and counters.