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 React Aria. Each example runs with local data.

Installation

Complete project setup with react/aria, then install these components:

pnpm exec frontfriend download collapsible button icon -f react --primitive aria

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, 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 isExpanded={open} onExpandedChange={setOpen}>
        <Button slot="trigger" variant="ghost" size="sm">
          <Icon name={open ? "ChevronDown" : "ChevronRight"} />
          <Icon name="Folder" />
          {node.name}
        </Button>
        <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}
      onPress={() => 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 React Aria API
button React Aria API
icon React Aria API