Listy

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

Usage

When to reach for Listy rather than one of its neighbors, the two props it will not run without, and the decisions it hands back to you.

When to use

A vertical list of records that scrolls inside its own area, optionally split into groups whose headers pin as you scroll. You return the whole row from itemRender.
Reach for it when the row count is large or unbounded, or when rows need sticky group headers. For other shapes, one of these fits better.
  • Table when the records share fields worth lining up in columns, so a reader can compare values down the page.
  • Descriptions for the fields of one record. Label and value pairs about a single thing are not a list of things.
  • Menu when the rows are navigation with a selected state that the app owns. A Listy row can be a link (the Navigation rows example is one), but a nav tree with selection and submenus is Menu's job.

rowKey and itemRender

rowKey and itemRender are required. Everything else is optional.
  • rowKey names a field of the item, or is a function that returns a key. Use a stable id, not the array index, because it is both the React key and the key scrollTo({ key }) looks up.
  • itemRender receives (item, index) and returns the whole row. There are no slots to fill. A rich row is kit components you compose, like Avatar, Flex, Typography and Tag.
  • The item type is inferred. TypeScript reads it from items, so itemRender gets a typed item and rowKey only accepts that item's fields. Writing <Listy<Member>> is optional.

What each row already has

Rows return bare content. Listy puts each row in an item element that already has padding, a bottom separator and a hover color, so padding on your row doubles it.
  • To drop the last separator, set classNames={{ item: "last:border-0" }}. The same class on your row never matches.
  • Truncation needs min-w-0. A flex column does not shrink below its content, so without min-w-0 a long line widens the row instead of ending in an ellipsis.
  • Show hover actions on focus too. Pair group-hover: with group-focus-within:, or keyboard users tab onto buttons they cannot see.

Virtual scrolling

With virtual on, only the rows on screen are in the page, so a list of 5,000 scrolls as smoothly as a short one. It is off by default.
  • virtual and sticky need a height. The height gives the list its own scroll area. Without it, virtual renders every row and sticky has no list scroll to pin headers in.
  • Virtual rows off screen are not in the page. With virtual on, :last-child matches the last rendered row, so leave out last:border-0. Drag and drop, find in page and screen readers only reach rendered rows, so keep virtual off for a sortable list.
  • A sortable list without virtual is fine for a few dozen rows. To reorder thousands, offer a “Move to” action instead of a longer drag.

Grouping and sticky headers

  • group takes key(item) and title(groupKey, items).
  • Group headers follow the order of the data. group collects rows by key wherever they sit, so the data does not need sorting. The headers appear in the order each group first shows up in items, so in a list read by date, put a row from the newest day first.
  • group.title receives the group's rows. Use them for a count or a subtotal in the header.
  • sticky pins each header to the top of the list until the next one pushes it out. It needs a height.
  • Keep a custom group header opaque. A sticky header scrolls over the rows, so a see-through classNames.groupHeader background lets them show through.

Driving the scroll

Listy owns its scroll container, so you reach it through the ref or onScroll.
  • Scroll with the ref. scrollTo takes an index, { key, align, offset }, { groupKey, align, offset } or { top, left }, and align is top, bottom or auto. Import the ListyRef type from antd, since the kit does not re-export it.
  • Load more from onScroll. It fires on the list's scroll container, with virtual on or off. Read scrollTop, scrollHeight and clientHeight from e.currentTarget, and add a Load more button for people who get around without scrolling.

Styling it

classNames and styles take an object keyed root, item and groupHeader, or a function that returns one. className and style land on the root.
  • classNames and styles functions see the whole list. Their one argument is { props } for the list, not for a row, so use them for list-wide state like “anything selected”. Style a single row from its own state inside itemRender.
  • Make a whole row a link. Set classNames.item to p-0 and put the padding on the link, so the click area reaches the separators. Add rtl:-scale-x-100 to a trailing chevron so it flips in right-to-left layouts.
  • Color status text with Typography.Text's type, not a hex value, so it follows the theme and dark mode.

What Listy leaves to you

Each of these belongs to the page around the list.
  • Listy replaces List. Ant Design's older List is deprecated and warns in development. Listy has no item slots, actions, header, footer, pagination, empty state or loading state, so build those in itemRender and around the list.
  • No empty state. An empty items renders an empty container. Check the length yourself and say why it is empty (nothing yet, or nothing matching this filter), since the two need different words and different next steps.
  • No loading state. Render a skeleton or a spinner around it, and keep the container's height steady so the page does not jump when the rows arrive.
  • No pagination. Listy scrolls instead, so onScroll plus a Load more button takes its place.
  • Sorting is up to your drag code. Listy has no sorting API, so reorder items in your drag handler. Native drag and drop does not work from the keyboard. For a list keyboard users can sort, use dnd-kit with a KeyboardSensor and a handle button with an aria-label.
  • There is no itemHeight or direction prop. Rows measure their own height, and the reading direction comes from AntuikitProvider.