File List Table
A list view with a sortable header, 20pt rows, alternating stripes and disclosure triangles.
@cupertinoui-pro/file-list-tableCupertinoUI Pro"use client"
import * as React from "react"
import type { FileNode } from "@/registry/macos/lib/file-tree"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { FileListTable } from "@/components/ui/file-list-table"
// The same JSON as the File List Columns demo: folders have children.
const initialFiles: FileNode[] = [
{
name: "Design",
modified: "Today at 09:41",
children: [
{ name: "Icons.sketch", kind: "Sketch Document", modified: "Today at 09:41", size: "18,2 MB" },
{ name: "Wallpaper.heic", kind: "HEIF Image", modified: "Today at 09:12", size: "4,1 MB" },
],
},
{
name: "Invoices",
modified: "Yesterday at 17:02",
children: [
{
name: "2026",
modified: "Yesterday at 17:02",
children: [
{ name: "September.pdf", kind: "PDF Document", modified: "1. Oct 2026 at 08:30", size: "212 KB" },
{ name: "October.pdf", kind: "PDF Document", modified: "Yesterday at 17:02", size: "198 KB" },
],
},
],
},
{ name: "Notes.txt", kind: "Plain Text", modified: "12. Oct 2026 at 12:29", size: "4 KB" },
{ name: "Roadmap.key", kind: "Keynote", modified: "8. Oct 2026 at 10:15", size: "2,4 MB" },
]
// Drag selected rows onto a folder to move them, press ⌘⌫ to delete them,
// and double-click a file to open it in a window.
export default function FileListTableDemo() {
const [files, setFiles] = React.useState(initialFiles)
const [opened, setOpened] = React.useState<FileNode | null>(null)
return (
<div className="h-72 w-full max-w-2xl overflow-hidden rounded-[12px] shadow-[0_0_0_1px_var(--mac-grid)]">
<FileListTable
files={files}
onFilesChange={setFiles}
onOpen={setOpened}
defaultExpanded={["Design", "Invoices"]}
defaultSelected={["Notes.txt"]}
columns="minmax(0,1fr) 160px 80px 120px"
className="h-full"
/>
<Dialog open={opened !== null} onOpenChange={(open) => !open && setOpened(null)}>
<DialogContent className="sm:max-w-xs">
<DialogHeader>
<DialogTitle>{opened?.name}</DialogTitle>
<DialogDescription>
{[opened?.kind, opened?.size].filter(Boolean).join(" - ")}
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</div>
)
}Installation
pnpm dlx shadcn@latest add @cupertinoui-pro/file-list-tablePart of CupertinoUI Pro: the CLI installs it with your license key, once the @cupertinoui-pro registry is in components.json.
Usage
import { FileListTable } from "@/components/ui/file-list-table"
import type { FileNode } from "@/lib/file-tree"
const files: FileNode[] = [
{
name: "Documents",
modified: "Today at 09:41",
children: [
{ name: "Notes.txt", kind: "Plain Text", modified: "Today at 09:41", size: "4 KB" },
],
},
]
<FileListTable files={files} defaultExpanded={["Documents"]} />files is plain JSON, the same structure FileListColumns takes, so both can show one tree. Click a disclosure triangle or double-click a folder to expand it; the arrow keys move the selection and open and close folders.
Like Finder, drag over rows to select them, from a row or from the empty space below; the table scrolls when the pointer leaves it. Shift-click selects a range, ⌘-click adds or removes a row, and neighbouring selected rows join into one highlight.
With onFilesChange, drag selected rows onto a folder to move them there, or onto a file to move them next to it; the folder lights up and a badge follows the pointer. Escape cancels. A folder can't move into itself, and a name that's taken gets a number, as in Finder. ⌘⌫ deletes the selection.
Right-click for Finder's menu: Open, Move to Trash, Rename, Duplicate, Copy and the tag colors; on empty space, New Folder. Renaming edits the name in place (Return also starts it), ⌘D duplicates and ⇧⌘N makes a folder. Without onFilesChange the menu only opens and copies.
Expanded folders and the selection are keys: the ids from the top of the tree down to the file, joined by "/", e.g. "Invoices/2026/October.pdf".
Columns are set once, as a CSS grid template, so the header and every row line up. Stripes are drawn behind the rows, so they continue into the empty space below.
Examples
Composition
Without files, build the header and rows from the parts yourself, e.g. for other columns or data that isn't a file tree.
"use client"
import * as React from "react"
import { FileTextIcon, FolderIcon } from "lucide-react"
import {
FileListTable,
FileListTableBody,
FileListTableCell,
FileListTableDisclosure,
FileListTableHead,
FileListTableHeader,
FileListTableRow,
} from "@/components/ui/file-list-table"
const files = [
{ name: "Design", kind: "Folder", modified: "Today at 09:41", size: "--", children: ["Icons.sketch", "Wallpaper.heic"] },
{ name: "Invoices", kind: "Folder", modified: "Yesterday at 17:02", size: "--", children: ["2026-09.pdf", "2026-10.pdf"] },
{ name: "Notes.txt", kind: "Plain Text", modified: "12. Oct 2026 at 12:29", size: "4 KB" },
{ name: "Roadmap.key", kind: "Keynote", modified: "8. Oct 2026 at 10:15", size: "2,4 MB" },
]
export default function FileListTableComposed() {
const [open, setOpen] = React.useState<string[]>(["Design"])
const [selected, setSelected] = React.useState("Notes.txt")
return (
<div className="h-72 w-full max-w-2xl overflow-hidden rounded-[12px] shadow-[0_0_0_1px_var(--mac-grid)]">
<FileListTable columns="minmax(0,1fr) 160px 70px 100px" className="h-full">
<FileListTableHeader>
<FileListTableHead sort="ascending" className="pl-[45px]">Name</FileListTableHead>
<FileListTableHead>Date Modified</FileListTableHead>
<FileListTableHead align="end">Size</FileListTableHead>
<FileListTableHead>Kind</FileListTableHead>
</FileListTableHeader>
<FileListTableBody>
{files.flatMap((file) => [
<FileListTableRow key={file.name} selected={selected === file.name} onClick={() => setSelected(file.name)}>
<FileListTableCell className="gap-1 pl-0.5">
{file.children ? (
<FileListTableDisclosure
open={open.includes(file.name)}
onClick={() =>
setOpen((current) =>
current.includes(file.name) ? current.filter((name) => name !== file.name) : [...current, file.name]
)
}
/>
) : (
<span className="w-3.5" />
)}
{file.children ? (
<FolderIcon className="size-4 fill-mac-blue text-mac-blue" />
) : (
<FileTextIcon className="size-4 text-mac-label-secondary" />
)}
{file.name}
</FileListTableCell>
<FileListTableCell className="text-mac-label-secondary">{file.modified}</FileListTableCell>
<FileListTableCell align="end" className="text-mac-label-secondary">{file.size}</FileListTableCell>
<FileListTableCell className="text-mac-label-secondary">{file.kind}</FileListTableCell>
</FileListTableRow>,
...(file.children && open.includes(file.name)
? file.children.map((child) => (
<FileListTableRow key={child} selected={selected === child} onClick={() => setSelected(child)}>
<FileListTableCell className="gap-1 pl-[17px]">
<span className="w-3.5" />
<FileTextIcon className="size-4 text-mac-label-secondary" />
{child}
</FileListTableCell>
<FileListTableCell className="text-mac-label-secondary">{file.modified}</FileListTableCell>
<FileListTableCell align="end" className="text-mac-label-secondary">1 MB</FileListTableCell>
<FileListTableCell className="text-mac-label-secondary">Document</FileListTableCell>
</FileListTableRow>
))
: []),
])}
</FileListTableBody>
</FileListTable>
</div>
)
}API Reference
Every part forwards the props of the underlying element. The props below are the ones CupertinoUI adds or changes.
FileNode
One file or folder in files: plain JSON, the same for every file list. Import the type from @/lib/file-tree.
| Prop | Type | Description |
|---|---|---|
name | string | The file name. |
id | string | Unique among its siblings, for keys; defaults to the name. Can't contain "/". |
children | FileNode[] | Makes it a folder; folders nest to any depth. Use [] for an empty folder. |
thumbnail | string | An image URL shown instead of the icon in the icon and gallery views, and as the preview. |
kind / size / created / modified | string | Shown as they are, so format them the way you want them displayed. |
info | Record<string, string> | More Information rows for the column and gallery previews, e.g. { Dimensions: "2007×2853" }. |
icon | string | A hint for renderIcon, e.g. "image" or "movie". |
tag | string | A Finder tag color: "red", "orange", "yellow", "green", "blue", "purple" or "gray". |
onOpen | (file: FileNode) => void | Called when this file or folder is opened: double-clicked, or ⌘O. |
FileListTable
| Prop | Type | Description |
|---|---|---|
files | FileNode[] | The tree to show, with Finder's Name, Date Modified, Size and Kind columns. Leave it out to compose the table from its parts. |
columns | stringDefault: "minmax(0,1fr) 181px 97px 121px" | A grid-template-columns value, e.g. "1fr 160px 100px". |
defaultExpanded | string[] | The keys of the folders that start open. |
expanded | string[] | The keys of the open folders, controlled. Update it from onExpandedChange. |
onExpandedChange | (expanded: string[]) => void | Called when a folder opens or closes. |
multiple | booleanDefault: true | Lets people select several rows: drag over them, Shift-click a range, ⌘-click single rows, Shift with the arrow keys or ⌘A. false allows one row. |
defaultSelected | string[] | The keys of the files that start selected. |
selected | string[] | The keys of the selected files, controlled. Update it from onSelectedChange. |
onSelectedChange | (keys: string[], files: FileNode[]) => void | Called when the selection changes, with the keys and files in row order. |
renderIcon | (file: FileNode) => ReactNode | The 16px icon of a file. Defaults to a folder or a document. |
renderActions | (file: FileNode) => ReactNode | Shown at the end of the name, e.g. an eject button for a volume. |
onFilesChange | (files: FileNode[], change: FileChange) => void | Turns on editing: dragging selected items onto a folder moves them, ⌘⌫ deletes them. Gets the new tree to store, and what changed: { action: "move", keys, to } or { action: "delete", keys }. |
onOpen | (file: FileNode, key: string) => void | Called when a file is opened: double-clicked, or ⌘O or ⌘↓ on the selection. Use it to show the file, e.g. in a new window. |
menuItems | (files: FileNode[], keys: string[]) => ReactNode | More ContextMenuItems for the right-click menu, after Finder's own. |
className | string | Extra classes, merged with the component's own. |
FileListTableHead
| Prop | Type | Description |
|---|---|---|
sort | "ascending" | "descending" | Shows the sort chevron and highlights the column. |
align | "start" | "end"Default: "start" | Text alignment. |
FileListTableBody
| Prop | Type | Description |
|---|---|---|
striped | booleanDefault: true | Alternating row stripes. |
FileListTableRow
| Prop | Type | Description |
|---|---|---|
selected | booleanDefault: false | Highlights the row. |
FileListTableCell
| Prop | Type | Description |
|---|---|---|
align | "start" | "end"Default: "start" | Text alignment. |
FileListTableDisclosure
The chevron of an outline view.
| Prop | Type | Description |
|---|---|---|
open | booleanDefault: false | Points down when open. |