List React component for Tailwind CSS v4
v1.1.0Updated in 1.13.0
A React list for settings, contacts, inboxes and orders: iOS grouped panels, selection, row menus, swipe actions and drag to reorder.
Tap or click a row to open it; navigation rows end in a chevron and tint on hover and press. On a phone, swipe a row left to uncover its trailing actions (Archive, Delete) or right for the leading ones (Mark read): the icons grow in as they come out, a short swipe snaps open, and swiping past 60% runs the outermost action with a haptic tick while a delete slides the row away and the list closes the gap. Only one row stays open; tapping it, tapping elsewhere or scrolling closes it. Long press a row on touch for a bottom sheet with the same actions; on desktop the ⋯ button (shown on hover or focus, always on touch) opens them as a menu, right click does too, and swipeWithMouse lets a mouse drag swipe. Selectable lists are one Tab stop: Up / Down, Home / End and PageUp / PageDown move, Space toggles, Shift + Space, Shift + arrows and Shift + click select a range, Ctrl / Cmd + A selects all, Escape clears, Enter opens. Shift + F10 or the Menu key opens the row menu and focus comes back to the row when it closes; Delete runs an action marked with the delete shortcut. Sortable lists show a grip: drag it (a long press on touch, so the page still scrolls), or focus it and press Space to lift, arrows to move, Space to drop, Escape to cancel; Alt + Up / Down on a focused row or grip moves it one step. Rows enter and leave on a soft spring; reduced motion turns movement into fades and snaps.
List is a Pro React component in the React table and list components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add list, together with Avatar, Button, Checkbox, Dropdown Menu, Empty State and 9 more and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @radix-ui/react-slot, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add listList is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import List from @/components/ui/list.
import { Archive, Bell, Globe, Trash2, User } from "lucide-react"
import { List, type ListGroupData } from "@/components/ui/list"
import { Switch } from "@/components/ui/switch"
export function Settings({ onDelete }: { onDelete: (id: string) => void }) {
const groups: ListGroupData[] = [
{
id: "account",
label: "Account",
items: [
{ id: "profile", title: "Profile", description: "emma.carter@northwind.app", icon: <User />, href: "/settings/profile" },
{ id: "notifications", title: "Notifications", icon: <Bell />, trailing: <Switch defaultChecked aria-label="Notifications" /> },
{ id: "language", title: "Language", icon: <Globe />, value: "English", href: "/settings/language" },
],
},
{
id: "clients",
label: "Recent clients",
footer: "Swipe a row to the left to archive it.",
items: [
{
id: "c01",
title: "Acme Corp",
description: "San Francisco · Nathan Price",
avatar: { name: "Acme Corp" },
value: "$12,480.00",
swipe: {
trailing: [
{ id: "archive", label: "Archive", icon: <Archive />, tone: "neutral", destructive: true, onSelect: () => onDelete("c01") },
{ id: "delete", label: "Delete", icon: <Trash2 />, tone: "danger", destructive: true, onSelect: () => onDelete("c01") },
],
},
},
],
},
]
return <List groups={groups} />
}
// parts, for rows the items API does not cover (same file, same look):
// ListRoot takes every appearance and behavior prop of List (variant, size, dividers, radius, tone, color,
// selectionMode + selection props, sortable + onReorder, swipeable / swipeWithMouse / fullSwipe, onAction(id),
// labels, classNames) plus keys / disabledKeys (read from the rows when left out); role list, or listbox;
// ListGroup { label, footer, refreshing } holds the variant's surface and animates rows in and out;
// ListItem takes every ListItemData field as a prop plus selected / onSelectedChange (standalone rows),
// asChild (your router Link as the row), interactive and size; children replace the generated content;
// content parts: ListItemLeading, ListItemIcon { tone, color, variant }, ListItemContent, ListItemTitle,
// ListItemDescription, ListItemTrailing (sits above the stretched pressable, so controls stay clickable);
// ListItemActions { items: MenuEntry[], open, onOpenChange, title, label } is the ⋯ menu (a sheet on phones);
// ListSortableHandle { label } is the grip of a sortable row (renders nothing outside a sortable list);
// SwipeActions { leading, trailing, disabled, swipeWithMouse, fullSwipe, onOpenChange, onRemovingChange }
// wraps any row; DEFAULT_LIST_LABELS holds the English strings to spread a translation over
import { Star } from "lucide-react"
import {
ListRoot, ListGroup, ListItem, ListItemLeading, ListItemIcon, ListItemContent, ListItemTitle,
ListItemDescription, ListItemTrailing, ListItemActions,
} from "@/components/ui/list"
export function Favorites({ rows }: { rows: { id: string; name: string; city: string; total: string }[] }) {
return (
<ListRoot variant="separated" size="lg" aria-label="Favorite clients">
<ListGroup label="Favorites">
{rows.map((row) => (
<ListItem key={row.id} id={row.id} title={row.name} href={`/clients/${row.id}`}>
<ListItemLeading>
<ListItemIcon tone="warning" variant="solid">
<Star />
</ListItemIcon>
</ListItemLeading>
<ListItemContent>
<ListItemTitle>{row.name}</ListItemTitle>
<ListItemDescription>{row.city}</ListItemDescription>
</ListItemContent>
<ListItemTrailing className="tabular-nums">{row.total}</ListItemTrailing>
<ListItemActions items={[{ id: "open", label: "Open", href: `/clients/${row.id}` }]} />
</ListItem>
))}
</ListGroup>
</ListRoot>
)
}Props
itemsListItemData[]The rows: { id, title, textValue?, description?, leading?, icon?, iconTone?, iconColor?, avatar?: { src?, name, status? }, trailing?, value?, badge?, href?, onSelect?, disabled?, tone?: "neutral" | "danger" | "brand", chevron?, actions?: MenuEntry[], swipe?: { leading?: SwipeAction[], trailing?: SwipeAction[] } }. trailing may hold controls (a Switch); the row then gets a stretched pressable so nothing nests in a button.
groupsListGroupData[]{ id, label?, footer?, items }[]: grouped rows with a small header and a footer note (iOS settings). Replaces items; sorting stays within a group.
variantdefault"inset""inset" | "plain" | "separated" | "card"inset: the iOS grouped panel (a raised surface with hairlines); plain: full-bleed rows with hairlines, no container; separated: each row its own raised panel with an 8px gap; card: one flat gray tile with hairlines.
sizedefault"md""sm" | "md" | "lg"Row height 44 / 56 / 64px with matching padding, avatars (32 / 40 / 48px) and icon tiles. Every size keeps a 44px target on touch.
dividersdefault"inset""full" | "inset" | "none"Hairlines between rows. inset starts them at the text, after the icon or avatar (iOS); full spans the row.
radiusdefault"md""none" | "sm" | "md" | "lg"Corner radius of the panel (inset, card) or of each row (separated): 0 / 12 / 16 / 24px. First and last rows follow it.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The selection color: the checkbox fill, the check mark and the highlight tint.
colordefault""stringAny CSS color for the selection; replaces the tone.
iconVariantdefault"soft""soft" | "solid" | "plain"Icon tiles: a soft tint (gray, or the icon tone), a solid fill (the iOS settings look, with iconColor), or the bare icon.
selectionIndicatordefault"auto""auto" | "checkbox" | "check" | "highlight"How a selected row shows: a leading checkbox, a trailing check (iOS pickers) or the row fill alone (with aria-selected). auto is a checkbox for multiple and a check for single.
actionsVisibilitydefault"hover""hover" | "always"The ⋯ row menu button: revealed on hover and focus with a mouse (always visible on touch), or always shown.
selectionModedefault"none""none" | "single" | "multiple"none: navigation rows in the Tab order (labelled groups become labelled lists). single / multiple: a listbox with roving focus, Space to toggle, Shift ranges and Ctrl / Cmd + A (through useSelection); give it an aria-label. Inside an option the ⋯ button is pointer-only and Shift + F10 opens the menu.
selectedKeysstring[]Controlled selection, in display order. Pair with onSelectedKeysChange.
defaultSelectedKeysdefault"[]"string[]Selection while uncontrolled.
onSelectedKeysChange(keys: string[]) => voidFires with the selected ids in display order.
loadingdefaultfalsebooleanSkeleton rows in the row shape (a leading circle when the rows have avatars or icons) inside the same container, with aria-busy.
refreshingdefaultfalsebooleanKeeps the rows and runs a thin indeterminate bar along the top edge of the first group (in the selection tone), with aria-busy. For pull to refresh and background reloads.
skeletonCountdefault4numberHow many skeleton rows show while loading.
emptyStateReactNodeShown when there are no rows; defaults to <EmptyState preset="empty" size="sm" title={labels.empty} />.
errorReactNodeReplaces the rows with the error EmptyState, this text as its description.
onRetry() => (void | Promise<unknown>)Adds Try again to the error state; a returned promise shows it loading.
showChevrondefaulttruebooleanNavigation rows (href or onSelect) end in a chevron. item.chevron overrides it per row.
sortabledefaultfalsebooleanDrag handles (dnd-kit): mouse after 8px, touch after a 250ms press, keyboard with Space and arrows, plus Alt + Up / Down on a focused row or grip. The lifted row floats with a shadow and a slight tilt; groups reorder within themselves.
onReorder(ids: string[], move: { id: string; from: number; to: number }) => voidFires after a drop or an Alt + Arrow move with the new order of that group. The list keeps the new order until you pass new items.
swipeabledefaulttruebooleanRows with swipe actions can be swiped on touch. Off: the actions stay in the ⋯ menu and the long-press sheet.
swipeWithMousedefaultfalsebooleanMouse drags swipe too. Off, desktop uses the ⋯ menu and right click, so text and clicks behave as usual.
fullSwipedefaulttruebooleanSwiping past 60% of the row runs the outermost action (iOS swipe to delete), with a haptic tick when it arms.
onAction(id: string, item: ListItemData) => voidRow activation (click, tap, Enter), after the item's own onSelect.
getActions(item: ListItemData) => (MenuEntry[] | undefined)Row menu entries per item, instead of item.actions. The menu also lists the row's swipe actions.
labelsPartial<ListLabels>English defaults (DEFAULT_LIST_LABELS): actions "More actions for {item}", select, dragHandle "Drag to reorder {item}", pickedUp, moved "{item} moved to position {n} of {total}.", dropped, cancelled, empty, loading, sheetTitle, cancel.
classNamesPartial<Record<ListSlot, string>>Extra classes per part: group, header, items, footer, item, row, select, leading, icon, body, content, title, description, trailing, value, chevron, actions, handle, swipe, swipeAction, overlay, refreshing, empty.
classNamestringClasses for the root. Override the swipe surface with [--list-bg:var(--card)] when the list sits on another surface.
Parts
ListRoot
componentThe outer part of a hand-built list. Takes every appearance and behavior prop of List (variant, size, dividers, radius, tone, color, selection, sortable with onReorder, swipe settings, onAction, labels, classNames) plus keys and disabledKeys, read from the rows when left out. Renders role list, or listbox when rows are selectable.
ListGroup
componentA group of rows on the variant's surface, with an optional header and footer note; it animates rows in and out.
labelReactNodeA small header above the rows.
footerReactNodeA note under the rows.
refreshingdefaultfalsebooleanAn indeterminate hairline on the top edge while the rows reload.
ListItem
componentOne row. Takes every ListItemData field as a prop (title, description, icon, avatar, value, badge, href, onSelect, actions, swipe...); children replace the generated content.
idstringThe row key for selection, reorder and swipe; generated when left out.
selectedbooleanSelected state for a standalone row outside a selectable ListRoot.
onSelectedChange(selected: boolean) => voidFires when a standalone row is toggled.
asChilddefaultfalsebooleanRenders the single child (your router Link or an anchor) as the row.
interactivebooleanForces the hover and press look; automatic when the row has href, onSelect, actions or selection.
size"sm" | "md" | "lg"Overrides the list's row size.
ListItemLeading
componentThe start slot of a row, for an icon tile, an avatar or a thumbnail.
ListItemIcon
componentThe rounded icon tile, sized by the row.
tone"neutral" | "brand" | "success" | "warning" | "danger" | "info"Tile tint; the neutral gray tile by default.
colorstringAny CSS color; replaces the tone.
variant"soft" | "solid" | "plain"A tinted tile, a filled tile (the iOS settings look) or just the icon.
ListItemContent
componentThe text column of a row, holding the title and the description.
ListItemTitle
componentThe row title, one line with an ellipsis.
ListItemDescription
componentThe muted line under the title.
ListItemTrailing
componentThe end slot for a value, a badge, a switch or a time. It sits above the row's stretched pressable, so controls inside stay clickable.
ListItemActions
componentThe more-actions menu of a row, a bottom sheet on phones. Without items the children render as they are, for your own buttons.
itemsMenuEntry[]The menu entries.
openbooleanControlled open state.
onOpenChange(open: boolean) => voidFires when the menu opens or closes.
titleReactNodeThe phone sheet's header.
labelstringaria-label of the menu button; {item} is the row name.
ListSortableHandle
componentThe drag grip of a sortable row; renders nothing outside a sortable list.
labelstringaria-label of the grip; defaults to the dragHandle label.
SwipeActions
componentThe swipe wrapper, usable around any row: actions sit underneath at both edges and uncover as the row slides.
leadingSwipeAction[]Revealed by a swipe toward the end (right in left-to-right layouts).
trailingSwipeAction[]Revealed by a swipe toward the start.
disabledbooleanTurns swiping off.
swipeWithMousebooleanLets mouse drags swipe too; defaults to the list's setting.
fullSwipebooleanPast 60% the outermost action runs; defaults to the list's setting.
onOpenChange(side: "leading" | "trailing" | null) => voidFires when a side opens or the row closes.
onRemovingChange(removing: boolean) => voidA destructive action slid the row out (true), or it came back (false).
DEFAULT_LIST_LABELS
constantThe English strings (more actions, select, drag handle, reorder announcements, empty, loading, sheet title, cancel). Spread it to translate a few. Type: ListLabels.
ListItemData
typeOne row of the items API: id, title, description, leading, icon with iconTone or iconColor, avatar, trailing, value, badge, href, onSelect, disabled, tone, chevron, actions and swipe.
SwipeAction
type{ id, label, icon?, tone?, color?, onSelect, destructive? }; a destructive action slides the row out and the caller removes the item.
Examples
iOS settings
Inset groups with solid icon tiles, switches, values with chevrons and a danger row.
Contacts picker
selectionMode="multiple": checkboxes, avatars, Shift ranges and select all.
Single choice
A trailing check that draws itself, like an iOS picker, with prices formatted in USD.
Sortable
Drag handles with a lifted overlay; Alt + Up / Down on a focused grip moves its row.
Separated orders
Each row its own panel, with an amount and a status badge.
Loading and empty
Skeletons in the row shape, then the rows; an empty list shows an EmptyState.
Sizes
44, 56 and 64px rows with 32, 40 and 48px avatars.
Compound parts
ListRoot, ListGroup and ListItem with a brand highlight on a flat card.
Dependencies
Files
- components/ui/list.tsx
FAQ
Is the React List component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install List?
Run npx wingo-ui@latest add list. It also adds the Wingo UI items it builds on (Avatar, Button, Checkbox, Dropdown Menu, Empty State and 9 more) and the npm packages @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities, @radix-ui/react-slot, lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/list.
How does List behave on phones?
On a phone, swipe a row left to uncover its trailing actions (Archive, Delete) or right for the leading ones (Mark read): the icons grow in as they come out, a short swipe snaps open, and swiping past 60% runs the outermost action with a haptic tick while a delete slides the row away and the list closes the gap.
See also
Used in
More React table and list components
Articles using this component
- React Settings Page: Section Nav, Save Bar, Danger Zone
Build it: screens and apps11 min read
- React Swipe to Delete: iOS Swipe Actions With Undo
Mobile UI in React9 min read
- dnd kit Mobile: Touch Drag That Lets the Page Scroll
Component guides11 min read
- React Pull to Refresh in a PWA Without Fighting the Browser
Mobile UI in React11 min read