docs / core / list

List

a vertical stack of items, each with an optional marker. the marker is decoration, so the text carries the meaning on its own.

intensity · recedesrsc · client

provenance

authored in usva

layer

core / primitives

intensity

recedes · safe anywhere, including dense surfaces

composition

feature lists, checklists and summaries inside a Card or sectiondivided when the rows are entries, plain when they are bulletsnot a menu and not navigation. those have their own rolesmeaning never lives in the marker alone

a11y

a real ul or ol · every marker is aria-hiddenjest-axe
live demo · try it out
  • Runs entirely on your machine
  • No tracking, no analytics
  • Open source, end to end
customize
asul or ol, by whether order carries meaning
markerdecorative marker on every item
dividedrule between items, not after the last
usagecopy
import { List, ListItem } from "usva/primitives/list";

<List marker={<CheckIcon />} divided>
  <ListItem>Runs entirely on your machine</ListItem>
  <ListItem>No tracking, no analytics</ListItem>
  <ListItem>Open source, end to end</ListItem>
</List>

props

proptypedefaultnotes
as"ul" | "ol""ul"pick by whether the order carries meaning.
markerReactNodedecorative marker on every item, hidden from assistive tech.
dividedbooleanfalserule between items, stopping at the last one.
ListItem markerReactNodeoverrides the shared marker for this item alone.

get it

npx shadcn add https://usva.build/r/list.jsoncopy
source · components/ui/list.tsxexactly what this command copiescopy
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";

const ListContext = React.createContext<React.ReactNode>(null);

export interface ListProps extends React.HTMLAttributes<HTMLElement> {
  as?: "ul" | "ol";
  /** Decorative marker rendered on every item. An item may override it. */
  marker?: React.ReactNode;
  /** Rule between items, stopping at the last one. */
  divided?: boolean;
}

export const List = React.forwardRef<HTMLElement, ListProps>(
  (
    { className, as: Comp = "ul", marker, divided, children, ...props },
    ref,
  ) => (
    <ListContext.Provider value={marker ?? null}>
      <Comp
        ref={ref as React.Ref<HTMLUListElement & HTMLOListElement>}
        className={cn(
          "flex list-none flex-col",
          divided &&
            "[&>li]:border-b [&>li]:border-border [&>li:last-child]:border-0",
          className,
        )}
        {...props}
      >
        {children}
      </Comp>
    </ListContext.Provider>
  ),
);
List.displayName = "List";

export interface ListItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
  /** Overrides the list's shared marker for this item alone. */
  marker?: React.ReactNode;
}

export const ListItem = React.forwardRef<HTMLLIElement, ListItemProps>(
  ({ className, marker, children, ...props }, ref) => {
    const shared = React.useContext(ListContext);
    const shown = marker ?? shared;
    return (
      <li
        ref={ref}
        className={cn(
          "flex items-start gap-3 px-3 py-4 text-muted",
          "transition-tint duration-fast ease-soft hover:text-ink motion-reduce:transition-none",
          className,
        )}
        {...props}
      >
        {shown != null && (
          <span
            data-list-marker=""
            aria-hidden="true"
            className="mt-0.5 shrink-0 text-accent-alt [&_svg]:size-4"
          >
            {shown}
          </span>
        )}
        <span className="min-w-0 flex-1">{children}</span>
      </li>
    );
  },
);
ListItem.displayName = "ListItem";