Installation
Complete project setup with react/base, then install these components:
pnpm exec frontfriend download collapsible button icon -f react --primitive base Folders use Collapsible and files use Button. Tab moves between the controls. Enter or Space opens a folder or marks a file row as selected. This is a nested disclosure list, so it does not claim the arrow-key behavior of an ARIA tree widget.
Usage
Complete example
import {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
type FileNode = {
name: string;
path: string;
children?: FileNode[];
};
const files: FileNode[] = [
{
name: "app",
path: "app",
children: [
{ name: "page.tsx", path: "app/page.tsx" },
{
name: "settings",
path: "app/settings",
children: [{ name: "page.tsx", path: "app/settings/page.tsx" }],
},
],
},
{ name: "package.json", path: "package.json" },
];
function FileTreeNode({
node,
selected,
onSelect,
}: {
node: FileNode;
selected: string;
onSelect: (path: string) => void;
}) {
const [open, setOpen] = React.useState(true);
if (node.children)
return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
<Icon name={open ? "ChevronDown" : "ChevronRight"} />
<Icon name="Folder" />
{node.name}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<div
style={{
paddingInlineStart: 20,
display: "grid",
justifyItems: "start",
}}
>
{node.children.map((child) => (
<FileTreeNode
key={child.path}
node={child}
selected={selected}
onSelect={onSelect}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
return (
<Button
variant={selected === node.path ? "secondary" : "ghost"}
size="sm"
aria-current={selected === node.path ? "true" : undefined}
onClick={() => onSelect(node.path)}
>
<Icon name="File" />
{node.name}
</Button>
);
}
export default function AiFileTreeExample() {
const [selected, setSelected] = React.useState("");
return (
<div
style={{
width: "100%",
maxWidth: 560,
minWidth: 0,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 16,
paddingBlock: 16,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
}}
>
<strong>Changed files</strong>
<div
role="group"
aria-label="Changed files"
style={{ display: "grid", justifyItems: "start" }}
>
{files.map((node) => (
<FileTreeNode
key={node.path}
node={node}
selected={selected}
onSelect={setSelected}
/>
))}
</div>
<p
role="status"
style={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clipPath: "inset(50%)",
whiteSpace: "nowrap",
border: 0,
}}
>
{selected ? "Selected: " + selected : ""}
</p>
</div>
);
} Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
collapsible | Base UI API |
button | Base UI API |
icon | Base UI API |