Skip to content
View as Markdown

ItemList ​

ItemList presents compact icon-and-label rows for navigation and actions. Use it for in-content navigation, tables of contents, or other lists that should share the Sidebar's list presentation without depending on its layout or providers.

It does not require AppShell, a router, or a Sidebar provider. Resource metadata, translations, permissions, and active-route matching remain the caller's responsibility.

Import ​

tsx
import { ItemList } from "@tailor-platform/app-shell";

Basic Usage ​

tsx
function BasicUsage() {
  const [view, setView] = useState("Overview");
  return (
    <>
      <ItemList.Root aria-label="Workspace">
        <ItemList.Item
          icon={<Home />}
          isActive={view === "Overview"}
          onClick={() => setView("Overview")}
        >
          Overview
        </ItemList.Item>
        <ItemList.Separator />
        <ItemList.Section title="Business">
          <ItemList.Item icon={<ShoppingCart />} onClick={() => setView("Orders")}>
            Orders
          </ItemList.Item>
          <ItemList.Group title="Products" icon={<Package />}>
            <ItemList.Item onClick={() => setView("All products")}>All products</ItemList.Item>
            <ItemList.Item onClick={() => setView("Categories")}>Categories</ItemList.Item>
          </ItemList.Group>
        </ItemList.Section>
      </ItemList.Root>
      <output>Current view: {view}</output>
    </>
  );
}

Sub-components ​

Sub-componentDescription
ItemList.RootA semantic ul; accepts standard list props
ItemList.ItemA compact icon-and-label row containing a button or supplied interactive element
ItemList.GroupA collapsible, indented child list; open by default
ItemList.SectionA labeled, non-collapsible list whose items retain their normal indentation
ItemList.SeparatorA decorative divider that remains a valid list child

ItemList.Item is for links and actions, not static rows. It requires onClick, render, or both; omitting both is a type error, including for a disabled Item. With onClick, it renders a button with type="button" by default. Supply an anchor or application link through render for navigation.

render replaces the interactive element inside the row, not the containing li. children supplies its label, and icon supplies an optional decorative leading icon. Standard button props, classes, attributes, and refs apply to the inner operation. A disabled Item is skipped by Tab and cancels clicks, including navigation through a supplied link.

tsx
function LinksAndActions() {
  const [checks, setChecks] = useState(0);
  const handleCheckNotifications = () => {
    setChecks((count) => count + 1);
  };
  return (
    <>
      <ItemList.Root aria-label="Quick access">
        <ItemList.Item icon={<Home />} render={<a href="https://example.com/help" />}>
          Help center
        </ItemList.Item>
        <ItemList.Item icon={<Bell />} onClick={handleCheckNotifications}>
          Check notifications
        </ItemList.Item>
      </ItemList.Root>
      <output>Notification checks: {checks}</output>
    </>
  );
}

For application navigation, use Link or NavLink from @tailor-platform/app-shell inside an AppShell-managed page. Do not import routing APIs directly from react-router or create a separate router around the list. The routes in this example belong to the documentation app; use your application's route paths in your own pages.

tsx
function AppShellRouting() {
  return (
    <ItemList.Root aria-label="Documentation pages">
      <ItemList.Item render={<Link to="/components/button" />}>Button documentation</ItemList.Item>
      <ItemList.Item render={<NavLink to="/components/item-list" end />}>
        ItemList documentation
      </ItemList.Item>
    </ItemList.Root>
  );
}

NavLink owns route matching and supplies aria-current="page" when matched; ItemList styles that attribute with the same emphasis as isActive. Use end when only the exact destination should be current. With a plain link, the caller supplies aria-current="page" when appropriate.

isActive remains a boolean for explicit visual emphasis, including local actions. It does not imply aria-current, selection state, a listbox role, or aria-selected. ItemList does not perform route matching.

Item Props ​

PropTypeDefaultDescription
iconReactNode—Optional decorative leading icon
childrenReactNode—Compact row label
renderReactElement—Replaces the inner button; required unless onClick is supplied
onClickMouseEventHandler<HTMLButtonElement>—Action handler; required unless render is supplied
isActivebooleanfalseExplicit visual emphasis without route matching or selection state
disabledboolean—Disables clicks and removes the operation from the tab order
classNamestring—Classes applied to the inner operation

Other standard button props, such as title and ARIA attributes, are forwarded to the inner operation. Put link-specific props such as href, target, and rel on the element passed to render.

Rows without an icon do not reserve icon space. To align a mixed list, explicitly pass icon={<span />} on a row that needs an empty icon slot. Lists with no icons keep their compact indentation.

Groups and Heading Hierarchies ​

expandOnClick defaults to true: clicking a group's heading toggles its child list, whether or not render is supplied. render only replaces the heading's main operation; it does not determine the expansion behavior. defaultOpen defaults to true; set it to false to start collapsed. Groups can be nested.

Set expandOnClick={false} to separate the heading's main operation from expansion. This requires onClick or render so the heading does not become an inert button. A separate button, named by the group title, expands and collapses the group. Use this for linked headings so navigation does not toggle the list.

onClick applies only to the heading's main operation. Clicking a child Item or the separate expand button does not invoke it. isActive and aria-current="page" control emphasis on the main heading operation, not the separate expand button.

For a table of contents, use a linked Group for a parent heading and Items for its child headings. Keep the list data aligned with the actual heading hierarchy and anchor IDs in the document.

tsx
function LinkedGroup() {
  return (
    <ItemList.Root aria-label="Contents">
      <ItemList.Group
        title="Installing"
        render={<a href="#installing" aria-label="Installing" />}
        expandOnClick={false}
      >
        <ItemList.Item render={<a href="#configuration" aria-label="Configuration" />}>
          Configuration
        </ItemList.Item>
      </ItemList.Group>
    </ItemList.Root>
  );
}

Group Props ​

PropTypeDefaultDescription
titleReactNode—Accessible group label, including text and optional decorations
iconReactNode—Optional decorative leading icon
renderReactElement—Replaces the main heading operation without changing expansion behavior
onClickMouseEventHandler<HTMLButtonElement>—Handler for the main heading operation only
expandOnClickbooleantrueToggle from the heading; false requires onClick or render and adds a separate expand button
isActivebooleanfalseExplicit visual emphasis for the heading operation
defaultOpenbooleantrueInitial uncontrolled expanded state
childrenReactNode—Nested Items, Groups, Sections, or Separators

Other standard li props, including className, apply to the containing group item, not its heading operation. onClick is the exception described above.

A Group's rendered label names its heading operation, separate expand button, and child list. Keep meaningful text in a rich title; ReactNode content is not converted into a string. Meaningful details such as counts can be read with the label, while purely decorative elements should use aria-hidden="true". An icon-only title needs screen-reader text. Do not put links or buttons inside the title, because the heading itself is interactive.

Sections and Separators ​

Use Group for hierarchy and Section for flat classification. A Section accepts a resolved string title and children, keeps child rows at their normal indentation, and does not add an interactive heading. Other standard li props apply to its outer item.

Neither Group nor Section requires wrapping its children in another ItemList.Root. Use ItemList.Separator between logical runs of items; it is decorative and renders a list item rather than an invalid direct div child of ul.

Styling and Accessibility ​

  • Keep labels concise; they truncate in narrow containers.
  • Use aria-label or aria-labelledby on Root when a list needs an accessible name.
  • Native links and buttons retain their keyboard interactions. Group buttons announce their expanded state and support Enter and Space.
  • Resolve translations before passing labels; ItemList does not translate them. Group titles can contain ReactNode content, while Section titles remain strings.
  • ItemList is an ordinary list, not a menu or selection widget. It does not provide arrow-key selection, search, reordering, or Sidebar icon-rail behavior.
  • Existing Sidebar components retain route metadata resolution, rail tooltips and flyouts, and drawer dismissal while sharing the underlying list presentation.