Listy

A list that scrolls, groups and virtualizes its rows, and leaves the content of every row to you.

Accessibility

Listy renders no semantics of its own, so everything below is markup you add around it.

What Listy renders

Listy is a layout primitive, not a widget. The scroll area, each row and each group header is a plain <div>, with no <ul>, <li>, role or aria-*, with or without virtual.
  • To assistive tech this is not a list, and nothing announces how many items there are. If the count matters, put it in visible text above the list, where it helps everyone.
  • antuikit adds no attribute and no class of its own.
  • Do not add role="list" around the rows. Listy builds the row elements, so you cannot give them role="listitem". Wrap the list in a labeled region instead and leave the rows as content.
  • With virtual on, off-screen rows are not in the page, so a screen reader cannot read ahead and find in page cannot find them. Keep the count down where you can.

Naming the list and its rows

Listy adds no names, so every name comes from the markup you return.
  • Put a visible heading above the list wherever there is room, and point a wrapping <section> or role="region" at it with aria-labelledby. Readers can jump to a heading, but not to a plain div with an aria-label.
  • A row's accessible name is its own text. Order the nodes you return so the most identifying one comes first: a reader hears the row in DOM order, not in the order the layout paints it.
  • Hide a decorative glyph in a row (a drag grip, a severity dot, an avatar beside a name that is already written out) with aria-hidden, so it is not read twice.

Actions inside a row

Row actions are markup you wrote, so every rule about buttons applies. Three of them are easy to miss here.
  • Name every icon-only action. A glyph is not a name. Give the button an aria-label, and in a long list say which row it acts on. Forty buttons named “Archive” are forty identical entries in a reader's element list.
  • Show hover actions on focus too. Pair group-hover: with group-focus-within:, or keyboard users tab onto buttons they cannot see.
  • Do not nest a button inside a whole-row link. Interactive content inside an <a> is invalid and the two hit areas fight. Either the row is a link and the actions sit outside it, or the row is not a link.

Keyboard reach

  • Listy adds no roving tab stop and no arrow-key navigation. Tab visits every focusable element in every rendered row, in order, so give a long list a heading or a skip link to jump past it.
  • A height makes a scroll area the keyboard may not reach. Listy gives it no tabIndex, and some browsers only let the keyboard scroll it through something focusable inside. When the rows have nothing to focus, leave out height and let the page scroll.
  • A whole-row link is one element, one tab stop and one name. With classNames.item set to p-0 and the padding on the link, the focus ring wraps the whole row.
  • Keep the focus ring on a row link, even where it looks heavy against the separator. A background change alone is a color signal, and color alone is not a focus indicator.

Loading and empty

Listy has no loading state and no empty state, so both are yours, and both are silent unless you say something.
  • Rows appended by a Load more fetch arrive with no announcement. Put the status (“Loading”, then “20 more loaded”) in a polite live region beside the list, and keep the trigger button focused rather than moving focus into the new rows.
  • An empty items renders an empty box that reads as nothing at all. Say why it is empty, and tell apart “nothing yet” from “nothing matches this filter”, because only one of the two has a next step.
  • Keep the container's height steady between the loading and loaded states. A list that grows under the pointer moves the row somebody was about to click.

Contrast and motion

  • Secondary row text (a timestamp, a preview line) is the usual failure. It is quieter than the title and still has to reach 4.5:1 against the row's background, and against its hover background too.
  • Severity, status and sign must not be carried by color alone. Pair the color with a word or a glyph: a red figure and a green figure are the same figure to a reader who cannot tell them apart.
  • Color status text with Typography.Text's type, not a hex value, so it follows the theme and dark mode.
  • The only motion Listy itself has is the scroll, and scrollTo jumps rather than animates. What needs guarding is anything you add on top, like a hover reveal, a row entering after a fetch or a reorder. Put those behind motion-safe:. Setting the motion token to false turns off the components' own transitions.