f-ui
Components

List

Presentational item list for Object Page rows — title, description, extra, and actions.

List is the Ant-class presentational item list: stacked rows with title, description, optional avatar, trailing extra, and row actions. Use it inside Object Page cards, dialogs, and other embedded collections. For a full List Report page with schema, selection, and chrome, use QueryList with view="list". For columnar compare and sort, use Table. Every collection job (Members, assigned keys, picker, index, attachments) is enumerated on List Surfaces.

When To Use

  • Embed a row collection on an Object Page, card, or dialog when each item is a title + description (optionally avatar, meta, actions).
  • Prefer Table when operators need to compare columns, sort, or scan dense fields side by side.
  • Prefer QueryList with view="list" when the list is the page — schema-driven List Report with filters, selection / batch, and data-list chrome.
  • Row ops belong on List.Item actions. Checkboxes and a batch bar are QueryList / Table, not this primitive — compose a Checkbox into leading only for a picker.
  • Do not import data-list internals into this primitive; keep List presentational and host-owned for fetch state.
This jobThis component
Members / activity inside Related ListList + actions
Grouped assigned keys (then Formily Save)List.Group
Dialog pickerList + leading Checkbox
Resource index with selection / batchQueryList view="list"not this page

Features

AreaBehavior
Rootul with data-slot="list"; optional split dividers (default on) and bordered frame
Itemli row with optional leading, main content, extra, and actions outside the title
MetaAvatar + title + optional meta chips + description (List.Meta)
GroupNested labelled group (List.Group) with sentence-case heading
Data modeOptional dataSource + renderItem + required rowKey
Many row opsCompose DataListRowActionsCell into actions (maxInline={2} + ). Do not ship an icon wall or a third overflow.

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/list.json
npx shadcn@latest add https://ui.isaacfei.com/r/list.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/list.json
bun x shadcn@latest add https://ui.isaacfei.com/r/list.json

With a namespace: npx shadcn@latest add @f-ui/list.

registryDependencies: utils.

Usage

import { List } from "@/components/f-ui/list/list";

<List>
  <List.Item>
    <List.Meta title="Ada Lovelace" description="Mathematician" />
  </List.Item>
</List>

Examples

Basic Rows

Two members with title and description — the default split dividers between rows.

  • Ada Lovelace
    Mathematician
  • Grace Hopper
    Rear admiral
"use client";

import { List } from "@/components/f-ui/list/list";

export function ListBasicDemo() {
  return (
    <List>
      <List.Item>
        <List.Meta title="Ada Lovelace" description="Mathematician" />
      </List.Item>
      <List.Item>
        <List.Meta title="Grace Hopper" description="Rear admiral" />
      </List.Item>
    </List>
  );
}

Grouped

A nested group with a sentence-case heading and labelled nested list for related tickets.

  • Tickets

    • INC-1042
      Printer offline on floor 3
    • INC-1048
      VPN access for new hire
"use client";

import { List } from "@/components/f-ui/list/list";

export function ListGroupedDemo() {
  return (
    <List>
      <List.Group heading="Tickets">
        <List.Item>
          <List.Meta title="INC-1042" description="Printer offline on floor 3" />
        </List.Item>
        <List.Item>
          <List.Meta title="INC-1048" description="VPN access for new hire" />
        </List.Item>
      </List.Group>
    </List>
  );
}

Actions vs Extra

extra holds muted metadata; actions holds a ghost icon Remove control outside the title link area.

  • Ada Lovelace
    ada@example.com
    Owner
  • Grace Hopper
    grace@example.com
    Member
"use client";

import { Trash2Icon } from "lucide-react";

import { List } from "@/components/f-ui/list/list";
import { IconGhostButton } from "@/demos/_shared/icon-ghost-button";

export function ListActionsDemo() {
  return (
    <List>
      <List.Item
        extra="Owner"
        actions={[
          <IconGhostButton key="rm" label="Remove">
            <Trash2Icon className="size-4" />
          </IconGhostButton>,
        ]}
      >
        <List.Meta title="Ada Lovelace" description="ada@example.com" />
      </List.Item>
      <List.Item
        extra="Member"
        actions={[
          <IconGhostButton key="rm" label="Remove">
            <Trash2Icon className="size-4" />
          </IconGhostButton>,
        ]}
      >
        <List.Meta title="Grace Hopper" description="grace@example.com" />
      </List.Item>
    </List>
  );
}

Many Row Actions

Five ghosts steal the title. Keep at most two frequent icons inline, put the rest in , and leave Remove trailing — the same Row Actions Column rule Table already locked. Compose DataListRowActionsCell into actions; do not invent a third overflow.

Wrong

  • Ada Lovelace
    ada@example.com
    Owner
  • Grace Hopper
    grace@example.com
    Member

Right

  • Ada Lovelace
    ada@example.com
    Owner
  • Grace Hopper
    grace@example.com
    Member
"use client";

import {
  CopyIcon,
  DownloadIcon,
  PencilIcon,
  Share2Icon,
  Trash2Icon,
} from "lucide-react";

import type { RowAction } from "@/components/f-ui/data-list-internals/row-actions/row-action-types";
import { useDataListViewI18n } from "@/components/f-ui/data-list-view/hooks/use-data-list-view-i18n";
import { DataListRowActionsCell } from "@/components/f-ui/data-list-view/row-actions-cell";
import { List } from "@/components/f-ui/list/list";
import { TooltipProvider } from "@/components/ui/tooltip";
import { DesignCompare } from "@/demos/_design/design-compare";
import { IconGhostButton } from "@/demos/_shared/icon-ghost-button";

type Member = {
  id: string;
  name: string;
  email: string;
  extra: string;
};

const MEMBERS: Member[] = [
  {
    id: "ada",
    name: "Ada Lovelace",
    email: "ada@example.com",
    extra: "Owner",
  },
  {
    id: "grace",
    name: "Grace Hopper",
    email: "grace@example.com",
    extra: "Member",
  },
];

const ROW_ACTIONS: RowAction<Member>[] = [
  {
    id: "edit",
    label: "Edit",
    icon: <PencilIcon className="size-3.5" />,
    onClick: () => {},
  },
  {
    id: "duplicate",
    label: "Duplicate",
    icon: <CopyIcon className="size-3.5" />,
    group: "secondary",
    onClick: () => {},
  },
  {
    id: "share",
    label: "Share",
    icon: <Share2Icon className="size-3.5" />,
    group: "secondary",
    onClick: () => {},
  },
  {
    id: "download",
    label: "Download",
    icon: <DownloadIcon className="size-3.5" />,
    group: "secondary",
    onClick: () => {},
  },
  {
    id: "remove",
    label: "Remove",
    icon: <Trash2Icon className="size-3.5" />,
    variant: "destructive",
    placement: "trailing",
    confirm: false,
    onClick: () => {},
  },
];

function IconWallList() {
  return (
    <List>
      {MEMBERS.map((member) => (
        <List.Item
          key={member.id}
          extra={member.extra}
          actions={[
            <IconGhostButton key="edit" label="Edit">
              <PencilIcon className="size-4" />
            </IconGhostButton>,
            <IconGhostButton key="duplicate" label="Duplicate">
              <CopyIcon className="size-4" />
            </IconGhostButton>,
            <IconGhostButton key="share" label="Share">
              <Share2Icon className="size-4" />
            </IconGhostButton>,
            <IconGhostButton key="download" label="Download">
              <DownloadIcon className="size-4" />
            </IconGhostButton>,
            <IconGhostButton key="remove" label="Remove">
              <Trash2Icon className="size-4" />
            </IconGhostButton>,
          ]}
        >
          <List.Meta title={member.name} description={member.email} />
        </List.Item>
      ))}
    </List>
  );
}

function OverflowList() {
  const { t } = useDataListViewI18n();

  return (
    <List>
      {MEMBERS.map((member) => (
        <List.Item
          key={member.id}
          extra={member.extra}
          actions={[
            <DataListRowActionsCell
              key="ops"
              row={member}
              actions={ROW_ACTIONS}
              display="inline"
              inlinePresentation="icon"
              maxInline={2}
              t={t}
            />,
          ]}
        >
          <List.Meta title={member.name} description={member.email} />
        </List.Item>
      ))}
    </List>
  );
}

/** Many row ops: icon wall vs DataListRowActionsCell (max 2 inline + overflow + trailing Remove). */
export function ListManyActionsDemo() {
  return (
    <TooltipProvider>
      <DesignCompare wrong={<IconWallList />} right={<OverflowList />} />
    </TooltipProvider>
  );
}
<List.Item
  extra="Owner"
  actions={[
    <DataListRowActionsCell
      key="ops"
      row={member}
      actions={rowActions}
      display="inline"
      inlinePresentation="icon"
      maxInline={2}
      t={t}
    />,
  ]}
>
  <List.Meta title={member.name} description={member.email} />
</List.Item>

Avatar And Meta Chips

Avatar sits in List.Meta. Status chips use meta beside the title — not a second description line.

  • Ada Lovelace
    Admin
    ada@example.com
    Owner
  • Grace Hopper
    Viewer
    grace@example.com
    Member
"use client";

import { List } from "@/components/f-ui/list/list";
import { StatusTag } from "@/components/f-ui/status-tag";

function Initials({ children }: { children: string }) {
  return (
    <span
      aria-hidden
      className="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium"
    >
      {children}
    </span>
  );
}

export function ListAvatarMetaDemo() {
  return (
    <List>
      <List.Item extra="Owner">
        <List.Meta
          avatar={<Initials>AL</Initials>}
          description="ada@example.com"
          meta={<StatusTag tone="info">Admin</StatusTag>}
          title="Ada Lovelace"
        />
      </List.Item>
      <List.Item extra="Member">
        <List.Meta
          avatar={<Initials>GH</Initials>}
          description="grace@example.com"
          meta={<StatusTag tone="neutral">Viewer</StatusTag>}
          title="Grace Hopper"
        />
      </List.Item>
    </List>
  );
}

Picker Checkboxes

A dialog or Transfer source may compose a Checkbox into leading. That is host state. It is not kit rowSelection and must not grow a batch footer. For mass ops on a resource index, use QueryList view="list".

  • Ada Lovelace
    ada@example.com
  • Grace Hopper
    grace@example.com
  • Alan Turing
    alan@example.com
"use client";

import { useState } from "react";

import { List } from "@/components/f-ui/list/list";
import { Checkbox } from "@/components/ui/checkbox";

const PEOPLE = [
  { id: "ada", name: "Ada Lovelace", email: "ada@example.com" },
  { id: "grace", name: "Grace Hopper", email: "grace@example.com" },
  { id: "alan", name: "Alan Turing", email: "alan@example.com" },
];

/** Dialog / Transfer-style picker: host Checkbox in `leading`. Not a batch bar. */
export function ListLeadingPickerDemo() {
  const [picked, setPicked] = useState<string[]>(["ada"]);

  return (
    <List>
      {PEOPLE.map((person) => {
        const checked = picked.includes(person.id);
        return (
          <List.Item
            key={person.id}
            leading={
              <Checkbox
                aria-label={`Select ${person.name}`}
                checked={checked}
                onCheckedChange={(value) => {
                  const on = value === true;
                  setPicked((current) =>
                    on
                      ? [...current, person.id]
                      : current.filter((id) => id !== person.id),
                  );
                }}
              />
            }
          >
            <List.Meta description={person.email} title={person.name} />
          </List.Item>
        );
      })}
    </List>
  );
}

title is a ReactNode. Pass a host <a> (or your router Link). List does not own navigation.

"use client";

import { List } from "@/components/f-ui/list/list";

/** Title is a host link. List does not own routing. */
export function ListTitleLinkDemo() {
  return (
    <List>
      <List.Item extra="Owner">
        <List.Meta
          description="ada@example.com"
          title={
            <a className="text-primary hover:underline" href="#ada">
              Ada Lovelace
            </a>
          }
        />
      </List.Item>
      <List.Item extra="Member">
        <List.Meta
          description="grace@example.com"
          title={
            <a className="text-primary hover:underline" href="#grace">
              Grace Hopper
            </a>
          }
        />
      </List.Item>
    </List>
  );
}

Every Slot On One Row

A picker of people uses checkbox in leading, avatar in List.Meta, title + chip, description, extra, and actions. Do not put a decorative icon in leading when the row already has an avatar — leading is the control, avatar is the identity.

  • Ada Lovelace
    Admin
    ada@example.com
    Owner
  • Grace Hopper
    Viewer
    grace@example.com
    Member
"use client";

import { useState } from "react";
import { Trash2Icon } from "lucide-react";

import { List } from "@/components/f-ui/list/list";
import { StatusTag } from "@/components/f-ui/status-tag";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

function Initials({ children }: { children: string }) {
  return (
    <span
      aria-hidden
      className="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium"
    >
      {children}
    </span>
  );
}

function RemoveButton({ label }: { label: string }) {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button aria-label={label} size="icon" type="button" variant="ghost">
          <Trash2Icon className="size-4" />
        </Button>
      </TooltipTrigger>
      <TooltipContent>{label}</TooltipContent>
    </Tooltip>
  );
}

const PEOPLE = [
  {
    id: "ada",
    initials: "AL",
    name: "Ada Lovelace",
    email: "ada@example.com",
    role: "Admin",
    extra: "Owner",
  },
  {
    id: "grace",
    initials: "GH",
    name: "Grace Hopper",
    email: "grace@example.com",
    role: "Viewer",
    extra: "Member",
  },
] as const;

/** Checkbox + avatar + title + chip + description + extra + actions — a real picker row. */
export function ListAllSlotsDemo() {
  const [picked, setPicked] = useState<string[]>(["ada"]);

  return (
    <TooltipProvider>
      <List>
        {PEOPLE.map((person) => {
          const checked = picked.includes(person.id);
          return (
            <List.Item
              key={person.id}
              actions={[
                <RemoveButton key="rm" label={`Remove ${person.name}`} />,
              ]}
              extra={person.extra}
              leading={
                <Checkbox
                  aria-label={`Select ${person.name}`}
                  checked={checked}
                  onCheckedChange={(value) => {
                    const on = value === true;
                    setPicked((current) =>
                      on
                        ? [...current, person.id]
                        : current.filter((id) => id !== person.id),
                    );
                  }}
                />
              }
            >
              <List.Meta
                avatar={<Initials>{person.initials}</Initials>}
                description={person.email}
                meta={
                  <StatusTag tone={person.role === "Admin" ? "info" : "neutral"}>
                    {person.role}
                  </StatusTag>
                }
                title={person.name}
              />
            </List.Item>
          );
        })}
      </List>
    </TooltipProvider>
  );
}

Data Source Map

dataSource + renderItem + required rowKey maps an array. bordered frames the list when it is not already inside Related List.

  • INC-1042
    Printer offline on floor 3
  • INC-1048
    VPN access for new hire
"use client";

import { List } from "@/components/f-ui/list/list";

const TICKETS = [
  { id: "INC-1042", summary: "Printer offline on floor 3" },
  { id: "INC-1048", summary: "VPN access for new hire" },
];

export function ListDataSourceDemo() {
  return (
    <List
      bordered
      dataSource={TICKETS}
      renderItem={(row) => (
        <List.Item>
          <List.Meta description={row.summary} title={row.id} />
        </List.Item>
      )}
      rowKey="id"
    />
  );
}

No Row Dividers

split={false} drops the default divide-y — use it for a tight activity stack, not for Members.

  • Order ORD-1001
    Changed status to Confirmed
    2m ago
  • Order ORD-1001
    Uploaded packing-list.xlsx
    1h ago
  • Members
    Added Grace Hopper
    Yesterday
"use client";

import { List } from "@/components/f-ui/list/list";

/** Tight activity rows without dividers. */
export function ListNoSplitDemo() {
  return (
    <List split={false}>
      <List.Item extra="2m ago">
        <List.Meta description="Changed status to Confirmed" title="Order ORD-1001" />
      </List.Item>
      <List.Item extra="1h ago">
        <List.Meta description="Uploaded packing-list.xlsx" title="Order ORD-1001" />
      </List.Item>
      <List.Item extra="Yesterday">
        <List.Meta description="Added Grace Hopper" title="Members" />
      </List.Item>
    </List>
  );
}

Composition

List
├─ List.Item
│  ├─ leading (optional checkbox / icon)
│  ├─ List.Meta (avatar, title, meta, description)
│  ├─ extra
│  └─ actions
└─ List.Group
   └─ List.Item …

Edge Cases And Errors

Empty Collection

A blank <List /> is not an Empty state. On an Object Page, set Related List status="empty". On a resource index, let QueryList own Empty. Do not render an empty ul and hope operators understand it.

Checkboxes

A Checkbox in leading is host picker state. It is not kit selection: there is no rowSelection, no selectedRowKeys, and no batch footer. For mass ops, use QueryList or Related List + Table.

dataSource Without renderItem / rowKey

Both are required when dataSource is set. Missing either throws. Prefer declarative List.Item children unless you are mapping a host array.

Loading And Error

List is presentational. Do not import data-list internals here. Host Related List status or QueryList for the triad.

API Reference

Props

List

PropTypeDefaultDescription
childrenReactNodeDeclarative rows / groups
dataSourcereadonly T[]Items to map with renderItem
renderItem(item: T, index: number) => ReactNodeRequired when dataSource is set. QueryList view="list" uses the same word with (row, ctx, defaultItem) — handle-aware; still return List.Item.
rowKeykeyof T | ((item: T) => string)Required when dataSource is set
splitbooleantrueRow dividers (divide-y)
borderedbooleanfalseOuter border + radius
classNamestringRoot ul class

List.Item

PropTypeDefaultDescription
childrenReactNodeUsually List.Meta
leadingReactNodeLeading control (checkbox, icon) outside the title
extraReactNodeTrailing muted meta
actionsReactNode[]Trailing action controls
classNamestringRow li class

List.Meta

PropTypeDefaultDescription
avatarReactNodeLeading media
titleReactNodePrimary label
metaReactNodeInline chips beside the title
descriptionReactNodeSecondary text
classNamestringMeta root class

List.Group

PropTypeDefaultDescription
headingReactNodeGroup heading (h3)
childrenReactNodeNested List.Item rows
classNamestringGroup li class

On this page