Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

File Tree

A nested file browser with expandable folders and selectable files.

Interactive preview

The shared preview uses React Base. The source below uses Base UI. Each example runs with local data.

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.

ComponentTarget reference
collapsible Base UI API
button Base UI API
icon Base UI API