# @pierre/trees
> An open source file tree rendering library for the web. Built for extreme performance on large trees, with React and vanilla JS APIs, SSR support, and customizable styling.
- Package: `@pierre/trees` on [npm](https://www.npmjs.com/package/@pierre/trees)
- GitHub: https://github.com/pierrecomputer/pierre
- Docs: https://trees.software/docs
## Overview
> **Warning:** **Trees is in beta.** Start from the public React, vanilla, and SSR entry
> points on this page. Expect refinements and small API changes between beta
> releases.
**Trees** uses one path-first model. The model works the same across React,
vanilla, and SSR hydration. Selection, focus, search, rename, drag and drop, Git
status, and row annotations all use canonical paths.
These docs are guide-first. First, select your runtime. Next, shape the tree
data before it reaches the UI. Then add search, item actions, styling, icons,
row signals, or SSR when you need them.
## Build with agents
### Agent skill
Install the
[`trees` agent skill](https://www.skills.sh/pierrecomputer/pierre/trees) with
the [Skills CLI](https://skills.sh/docs/cli) for access to the entire API and
common recipes, regardless of your integration method.
### Prompt an agent
Alternatively, you can copy-paste this prompt to your agent to install the skill
and point it to these docs as plain-text.
The "For Agents" button on the [home page](/) copies the same prompt.
### Plain-text docs
Our documentation is also available in condensed Markdown text, available in two
versions:
- [llms.txt](/llms.txt) is a short index of the sections
- [llms-full.txt](/llms-full.txt) is the everything in the docs — prose, API
tables, and code examples
Copy and paste as needed, or provide the URLs to your agent.
**Agent Prompt** (`prompt.md`):
```text
Set up @pierre/trees in this project.
Install its agent skill first so you have the full API reference:
npx skills add pierrecomputer/pierre --skill trees
Then follow that skill to add @pierre/trees.
Docs: https://trees.software/docs
Full reference for LLMs: https://trees.software/llms-full.txt
```
**Agent Skill Install** (`agent-skill.sh`):
```bash
npx skills add pierrecomputer/pierre --skill trees
```
`@pierre/trees` gives you one path-first model and two primary runtime entries:
a thin React layer in [`@pierre/trees/react`](#get-started-with-react) and the
vanilla class in [`@pierre/trees`](#get-started-with-vanilla).
### Choose your integration
#### 1. Path-first identity
Use canonical path strings as the public identity for each item in the tree. For
example, `src/components/Button.tsx` is more than a label on screen. It is the
value that you read from selection state. It is the path that you focus in code.
It is also the target that you rename or move later.
To learn the shared terms behind this rule, read
[Shared concepts](#shared-concepts).
#### 2. React vs Vanilla JS
Use the React entry point when your UI is already in React. For more
information, read the [getting started with React](#get-started-with-react)
guide.
Use the vanilla class in two cases. Use it when your app is not React-based.
Also use it when another framework must own the lifecycle around an imperative
model. For more information, read
[getting started with vanilla JS](#get-started-with-vanilla).
#### 3. Understand tree-shape
Both runtimes use the same tree data. Small examples can start with raw `paths`.
But real application trees must move to prepared input. This step prevents the
client from doing the shaping work on every load. Read
[Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering) after
your runtime quickstart. This step applies to both React and vanilla JS.
**Choose Integration React Example** (`project-tree.tsx`):
```tsx
import { FileTree, useFileTree } from '@pierre/trees/react';
export function ProjectTree({ paths }: { paths: readonly string[] }) {
const { model } = useFileTree({ paths, search: true });
return ;
}
```
**Choose Integration Vanilla Example** (`mount-tree.ts`):
```typescript
import { FileTree } from '@pierre/trees';
const fileTree = new FileTree({
paths: ['README.md', 'src/index.ts', 'src/components/Button.tsx'],
search: true,
});
const container = document.getElementById('project-tree');
if (container instanceof HTMLElement) {
fileTree.render({ fileTreeContainer: container });
}
```
### Get started with React
Use the React entry point when your UI is already in React. The hook creates one
stable tree model. The component mounts that model into the host element.
#### Install `@pierre/trees`
Use the package root for the vanilla runtime. Use the `/react` entry point for
the React wrapper.
#### Create the model with `useFileTree(...)`
`useFileTree(...)` from `@pierre/trees/react` creates the model one time for the
component lifetime. Later option changes do not update the model. Update the
model through methods when the tree data or runtime behavior changes after
mount. The methods include `resetPaths(...)`, `setComposition(...)`,
`setGitStatus(...)`, and `setIcons(...)`.
For small trees, pass raw `paths`. For scalable trees, use `preparedInput`.
Create `preparedInput` on the server or another non-UI boundary.
Read [Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering)
after this quickstart if you must still decide how to shape that input.
#### Render with ``
`` is a thin React wrapper over the model. It mounts the tree into
the host element. It forwards normal host props such as `className` and `style`.
It also hydrates existing server output when you pass `preloadedData` later.
Keep the mental model simple. The model owns the tree state. The React component
renders it.
#### Read and update tree state through the model
React code reads snapshots from the model through selector hooks. React code
writes back through model methods.
Use `useFileTreeSelector(model, selector, equality?)` when sibling UI needs a
custom derived snapshot. This hook does not rerender on unrelated tree changes.
For the shared interaction terms, read
[Navigate selection, focus, and search](#navigate-selection-focus-and-search)
and [React API](#react-api).
#### Use simple `paths` input only when the tree is small
Raw `paths` is the low-ceremony path for demos, tests, and very small static
trees. It is not the scalable default. Move the shape or sort work out of the UI
when the tree becomes expensive on the client.
#### Move to prepared input before the tree gets expensive
Use this recommended scale path:
1. Load canonical paths on the server or another non-UI boundary.
2. Prepare the tree input one time.
3. Pass `preparedInput` into `useFileTree(...)`.
Use `preparePresortedFileTreeInput(...)` when the server already knows the final
order. It is the highest-performance prepared-input variant. The client can skip
both the shaping work and the extra sorting work.
#### Add SSR later when first paint matters
Hydration builds on top of the same model-first React story. The client still
calls `useFileTree(...)`. The React wrapper still renders the same model. The
one difference is that the tree starts from preloaded server output.
Continue with [SSR](#ssr) and [SSR API](#ssr-api) when you need that flow.
**React Quickstart Install** (`install.sh`):
```bash
pnpm add @pierre/trees
# npm: npm install @pierre/trees
# bun: bun add @pierre/trees
# yarn: yarn add @pierre/trees
```
**React Quickstart Project Tree** (`project-tree.tsx`):
```tsx
import { FileTree, useFileTree } from '@pierre/trees/react';
import type { FileTreePreparedInput } from '@pierre/trees';
interface ProjectTreeProps {
preparedInput: FileTreePreparedInput;
}
export function ProjectTree({ preparedInput }: ProjectTreeProps) {
const { model } = useFileTree({
preparedInput,
search: true,
initialExpandedPaths: ['src', 'src/components'],
});
return (
);
}
```
**React Quickstart Searchable Tree** (`searchable-tree.tsx`):
```tsx
import {
FileTree,
useFileTree,
useFileTreeSearch,
useFileTreeSelection,
} from '@pierre/trees/react';
export function SearchableTree({ paths }: { paths: readonly string[] }) {
const { model } = useFileTree({
paths,
fileTreeSearchMode: 'hide-non-matches',
search: true,
});
const selectedPaths = useFileTreeSelection(model);
const search = useFileTreeSearch(model);
return (
);
}
```
### Get started with vanilla
Use the vanilla runtime in two cases. Use it when your app is not React-based.
Also use it when another framework must own the lifecycle around an imperative
tree model. `new FileTree(...)` creates the model. `render(...)` or
`hydrate(...)` attaches the model to the DOM.
#### Install `@pierre/trees`
#### Create the model with `new FileTree(...)`
The class instance is the runtime entry point and the state surface. For small
trees, pass raw `paths`. For scalable trees, use `preparedInput`. Create
`preparedInput` outside the UI.
#### Render into a host element
`render({ fileTreeContainer })` mounts the model into an existing host element.
Use `render({ containerWrapper })` instead when the runtime must create the host
for you.
Keep the boundary clear. The model owns the tree state. The mounted host only
renders that state. Do not read the DOM to find the selected item or the current
focus. Read and update those values through the model.
#### Read and update tree state through the model
The instance gives you direct read methods, item handles, and imperative
controls.
Call explicit model methods when the surrounding data changes. The methods
include `resetPaths(...)`, `setComposition(...)`, `setGitStatus(...)`, and
`setIcons(...)`. Do not build the model again in place.
#### Use simple `paths` input only when the tree is small
Raw `paths` is the low-ceremony path for demos, tests, and small static trees.
It is not the recommended setup for repo-scale or workspace-scale trees.
#### Move to prepared input before the tree gets expensive
Use this recommended scale path:
1. Load canonical paths outside the UI.
2. Prepare the tree input one time.
3. Construct `new FileTree({ preparedInput, ... })`.
Use `preparePresortedFileTreeInput(...)` when the server or indexer already
knows the final order. It is the better fit because the client can skip the
extra sorting work.
#### Add SSR later when server rendering matters
Hydration builds on top of the same class-first runtime. The client still
creates `new FileTree(...)`. But the client does not render fresh markup.
Instead, it attaches that model to server-rendered tree output with
`hydrate({ fileTreeContainer })`.
Continue with [SSR](#ssr) and [SSR API](#ssr-api) when you need that flow.
#### Advanced note: wrapping the vanilla model in another framework
Keep the same ownership boundary when you wrap the vanilla model in another
framework:
- create and own the `FileTree` instance from that framework's lifecycle
- mount or unmount around the instance
- keep all tree reads and writes on the model
This approach keeps React as the only first-class wrapper surface. It does not
change how the model works.
**Vanilla Quickstart Imperative Usage** (`imperative-usage.ts`):
```typescript
const fileTree = new FileTree({
paths: ['README.md', 'src/index.ts', 'src/components/Button.tsx'],
search: true,
});
fileTree.render({ fileTreeContainer: container });
fileTree.focusPath('src/index.ts');
fileTree.openSearch('button');
const selectedPaths = fileTree.getSelectedPaths();
const matchingPaths = fileTree.getSearchMatchingPaths();
const focusedPath = fileTree.getFocusedPath();
const buttonItem = fileTree.getItem('src/components/Button.tsx');
buttonItem?.select();
```
**Vanilla Quickstart Install** (`install.sh`):
```bash
pnpm add @pierre/trees
# npm: npm install @pierre/trees
# bun: bun add @pierre/trees
# yarn: yarn add @pierre/trees
```
**Vanilla Quickstart Mount Project Tree** (`mount-project-tree.ts`):
```typescript
import { FileTree, type FileTreePreparedInput } from '@pierre/trees';
export function mountProjectTree(
container: HTMLElement,
preparedInput: FileTreePreparedInput
) {
const fileTree = new FileTree({
preparedInput,
search: true,
initialExpandedPaths: ['src', 'src/components'],
});
container.style.height = '320px';
fileTree.render({ fileTreeContainer: container });
return fileTree;
}
```
### Shape tree data for fast rendering
Shape the tree before it reaches the UI when the dataset is large. Then this
out-of-UI preparation is worth the effort. Trees treats prepared input as a
first-class public concept. It is not an internal optimization trick.
#### Start with server-prepared input for scalable trees
`prepareFileTreeInput(...)` lets the client skip repeated tree-shape work. Do
this preparation on the server, a loader, or another non-UI boundary. That
boundary already has the full path list.
The client still uses the same path-first model. Only the expensive preparation
step moves earlier.
#### Pass `preparedInput` into the runtime
Both primary runtimes use the same prepared payload shape.
#### Use simple `paths` input only for small trees
Raw `paths` is still the right choice for small demos, tests, and very small
static trees.
This is the easy start path, not the scale-oriented default. Move the
preparation work out of the client when the tree grows.
#### Presorted input is the highest-performance prepared-input path
Use `preparePresortedFileTreeInput(...)` when your server or indexer already
knows the final order. It skips the tree-shape work. It also skips the extra
sort work that a normal prepared-input pass applies.
Use this path when the backend, build step, or cached index already owns the
sort order. Do not write the default order again in the client only to reach the
presorted path.
#### Prepare on the server, render on the client
Use this simple split:
1. Load canonical paths outside the UI.
2. Prepare the input one time.
3. Pass `preparedInput` into React, vanilla, or SSR hydration.
This split reduces client CPU work. It makes the startup cost more predictable.
It also lets the same prepared payload feed every runtime.
#### Keep client-side sorting and preparation secondary
Sometimes the data exists only in the browser. Sometimes custom order must run
there. Trees supports both cases, but they are the exception path. Start with
prepared input when the app can move the work out of the UI. Use client-side
shaping only as a fallback.
To learn the shared contract behind these inputs, read
[Shared concepts](#shared-concepts). For the scale-oriented version of this
guidance, continue with
[Handle large trees efficiently](#handle-large-trees-efficiently).
**Shape Tree Data Prepare Loader** (`load-project-tree-input.ts`):
```typescript
import { prepareFileTreeInput } from '@pierre/trees';
export async function loadProjectTreeInput(projectId: string) {
const paths = await fetchProjectPaths(projectId);
return prepareFileTreeInput(paths, {
flattenEmptyDirectories: true,
});
}
```
**Shape Tree Data Presorted** (`presorted.ts`):
```typescript
import { preparePresortedFileTreeInput } from '@pierre/trees';
const preparedInput = preparePresortedFileTreeInput([
'README.md',
'src/index.ts',
'src/components/Button.tsx',
]);
```
**Shape Tree Data React Tree** (`react-tree.tsx`):
```tsx
import { FileTree, useFileTree } from '@pierre/trees/react';
import type { FileTreePreparedInput } from '@pierre/trees';
export function ReactTree({
preparedInput,
}: {
preparedInput: FileTreePreparedInput;
}) {
const { model } = useFileTree({ preparedInput });
return ;
}
```
**Shape Tree Data Small Paths** (`small-paths.ts`):
```typescript
const fileTree = new FileTree({
paths: ['README.md', 'src/index.ts', 'src/components/Button.tsx'],
});
```
**Shape Tree Data Vanilla Mount** (`mount-vanilla-tree.ts`):
```typescript
import { FileTree, type FileTreePreparedInput } from '@pierre/trees';
export function mountVanillaTree(
container: HTMLElement,
preparedInput: FileTreePreparedInput
) {
const fileTree = new FileTree({ preparedInput });
container.style.height = '320px';
fileTree.render({ fileTreeContainer: container });
return fileTree;
}
```
### Navigate selection, focus, and search
The tree model tracks three user-facing states: selection, focus, and the
visible row set. Search builds on the same model. It does not add a separate
identity system.
#### Start with the interaction state model
Selection, focus, keyboard movement, and search all use canonical paths. The
visible tree can change when a branch collapses or when search removes rows.
Your app still uses the same path values.
For this reason, the public APIs return path-based results. Selected paths are a
`readonly string[]`. Focused items are path strings. Search matches are path
strings.
#### Selection and focus are related but different
Focus tells you where keyboard actions land. Selection tells you which rows the
app treats as chosen. The two often move together. But they are not the same.
Read the selected paths when you need the multi-select state in surrounding UI.
Read the focused path or item handle when you need to know where a rename,
keyboard action, or command lands next.
#### Keyboard navigation follows the visible tree
Keyboard movement works on the visible, expanded tree. Expand or collapse a
branch to change which row receives focus next. Search changes the same visible
tree. So search also changes where keyboard movement lands.
For this reason, Trees exposes focus and selection through the model. It does
not use DOM order or row indexes.
#### Add search on top of that same state
Search matches the same path-first tree data. It changes what stays visible. But
it does not add a second identity model. Selection and focus still use paths.
The visible window changes around the current query.
#### Guide default: `hide-non-matches`
Start with `hide-non-matches`. Change it only when the product needs unrelated
branches on screen. This mode fits the common "find the thing I want" workflow.
It keeps the visible tree close to the current search intent.
Trees also supports `collapse-non-matches` and `expand-matches`. Use those modes
when the surrounding UI needs more context. But keep `hide-non-matches` as the
default mental model. The shared meaning of all three modes is in
[Shared concepts](#shared-concepts).
#### React integration notes
React reads from the model through selector hooks. React writes back through
model methods.
Use `useFileTreeSelector(...)` when sibling UI needs a custom derived snapshot.
#### Vanilla integration notes
Vanilla code connects the same behavior to the class instance.
The DOM host is not the source of truth. The model is the source of truth.
#### What to read next
- For runtime-specific lookup, read [React API](#react-api) or
[Vanilla API](#vanilla-api).
- For shared search-mode semantics, read
[Shared concepts](#shared-concepts-search-mode-semantics).
- For row-level editing workflows that build on this focus model, continue with
[Rename, drag, and trigger item actions](#rename-drag-and-trigger-item-actions).
**Navigate React Search** (`search-panel.tsx`):
```tsx
import {
FileTree,
useFileTree,
useFileTreeSearch,
useFileTreeSelection,
} from '@pierre/trees/react';
export function SearchPanel({ paths }: { paths: readonly string[] }) {
const { model } = useFileTree({
paths,
search: true,
fileTreeSearchMode: 'hide-non-matches',
});
const selectedPaths = useFileTreeSelection(model);
const search = useFileTreeSearch(model);
return (
{selectedPaths.length} selected
);
}
```
**Navigate Vanilla Search** (`vanilla-search.ts`):
```typescript
const fileTree = new FileTree({
paths,
search: true,
fileTreeSearchMode: 'hide-non-matches',
});
fileTree.render({ fileTreeContainer: container });
searchInput.addEventListener('input', () => {
fileTree.setSearch(searchInput.value);
});
const selectedPaths = fileTree.getSelectedPaths();
const focusedPath = fileTree.getFocusedPath();
const matchingPaths = fileTree.getSearchMatchingPaths();
```
### Rename, drag, and trigger item actions
This guide covers the main row-level editing workflows. Rename comes first. Drag
and drop comes second. Optional command surfaces, such as context menus, come
last. Every callback stays path-first.
#### Start with direct row actions
The core row workflows are:
1. rename in place
2. drag and drop
3. optional context-menu commands
Learn these flows before you use a generic mutation inventory. Users care about
three things. They care about which path moved. They care about which path
changed its name. They care about which row stays focused next.
#### Rename items in place
Enable `renaming` when users must rename rows inline. The common policy hooks
are:
- `canRename(item)` to block protected files or folders
- `onRename(event)` to respond to the final path change
- `onError(error)` to show invalid rename attempts
The rename event reports three values. It reports the source path. It reports
the destination path. It reports whether the item was a folder.
#### Move items with drag and drop
Enable `dragAndDrop` when users must move files or folders directly in the tree.
The practical hooks are:
- `canDrag(paths)` to lock specific paths
- `canDrop(event)` to reject invalid destinations
- `onDropComplete(event)` for persistence or adjacent UI updates
- `onDropError(error, event)` for visible failures
Drop callbacks report the dragged paths and the resolved target shape. They do
not ask you to find the DOM rows yourself.
#### Combine rename and drag safely
A common editable project tree enables both `renaming` and `dragAndDrop`. Then
it adds policy guards for protected paths.
Typical rules include:
- block rename for root config files such as `package.json`
- reject drops into generated directories such as `dist/`
- show rename and drop failures outside the tree; do not fail silently
#### Add a context menu as an optional command surface
Context menus are secondary command surfaces over the same model. Keep rename
and drag available. Do not require the menu.
In React, the wrapper can render the menu content for you:
In vanilla, use `composition.contextMenu.render` to supply the menu element
directly from the runtime config.
#### Lock window scroll while a menu is open
The tree blocks scrolling inside its own pane while a context menu is open. This
keeps the menu on its row. But the tree cannot reach the scroll containers that
your page owns. A portaled menu attaches to a point in the viewport. So the menu
disconnects from its row when the window scrolls. Lock the window scroll for the
menu's lifetime. Release the lock when the menu closes:
Some menu primitives have a modal mode. Radix and libraries built on it, such as
shadcn/ui, lock scroll for you when the menu is modal. Apply the manual lock for
a non-modal menu. The docs demos on this site apply this manual lock.
#### Keep keyboard and focus behavior predictable
The focused row is the anchor for rename and keyboard commands. Restore focus in
a predictable way when a menu opens and closes. Keyboard users must reach the
main actions without pointer-only gestures.
#### The tree owns the interaction surface. Your app owns persistence.
Trees emits path-based rename and drop events. Your app decides whether to save
those changes. It can save them to a server, local state, or another boundary.
Keep this separation clear.
To learn the shared terms behind those events, read
[Shared concepts](#shared-concepts-mutation-vocabulary). For runtime lookup,
read [React API](#react-api) or [Vanilla API](#vanilla-api).
**Rename Drag Context Menu** (`context-menu.tsx`):
```tsx
const { model } = useFileTree({
paths,
composition: {
contextMenu: {
enabled: true,
triggerMode: 'both',
buttonVisibility: 'when-needed',
},
},
renaming: true,
});
(
)}
/>;
```
**Rename Drag Drag And Drop** (`drag-and-drop.ts`):
```typescript
const fileTree = new FileTree({
paths,
dragAndDrop: {
canDrag: (draggedPaths) => draggedPaths.includes('package.json') === false,
canDrop: ({ target }) => target.directoryPath !== 'dist/',
onDropComplete: ({ draggedPaths, target }) => {
console.log(
'Moved',
draggedPaths,
'to',
target.directoryPath ?? '(root)'
);
},
onDropError: (message) => {
console.error(message);
},
},
});
```
**Rename Drag Rename** (`rename.tsx`):
```tsx
const { model } = useFileTree({
paths,
renaming: {
canRename: (item) => item.path !== 'package.json',
onRename: ({ sourcePath, destinationPath }) => {
console.log(`Renamed ${sourcePath} -> ${destinationPath}`);
},
onError: (message) => {
console.error(message);
},
},
});
model.startRenaming('src/index.ts');
```
**Rename Drag Scroll Lock** (`window-scroll-lock.tsx`):
```tsx
// Hide the window scrollbar and compensate for its width so the page
// content does not shift under the already-positioned menu.
function lockWindowScroll(): () => void {
const { body, documentElement } = document;
const scrollbarWidth = window.innerWidth - documentElement.clientWidth;
const previousOverflow = body.style.overflow;
const previousPaddingRight = body.style.paddingRight;
body.style.overflow = 'hidden';
if (scrollbarWidth > 0) {
body.style.paddingRight = `${scrollbarWidth}px`;
}
return () => {
body.style.overflow = previousOverflow;
body.style.paddingRight = previousPaddingRight;
};
}
// React: menu components mount exactly while the menu is open, so locking on
// mount and releasing on unmount covers the menu's whole lifetime.
function useWindowScrollLock() {
useEffect(() => lockWindowScroll(), []);
}
```
### Style and theme the tree
Start with the host element. Then move inward. Host styles control the outer
panel. CSS variables control most of the tree appearance.
`themeToTreeStyles(...)` maps an editor-like theme into the same variable
system. `unsafeCSS` is the escape hatch when the supported surfaces cannot reach
a narrow case.
#### Start with host styling
The host element is the outer panel boundary. Use it for width, height, borders,
radius, background, and layout placement.
In React, pass normal host props to ``.
In vanilla, style the mounted host element that you already own. After mount,
`getFileTreeContainer()` returns that element when runtime code must update it.
#### Use CSS variables for most visual changes
CSS variables are the main public styling surface inside the shadow root. Use
them before you inject custom CSS.
This approach keeps the normal fallback chain in place:
1. explicit override variables
2. `--trees-theme-*` variables from theme helpers
3. library defaults
For the token families behind those variables, read
[Styling and theming](#styling-and-theming).
#### Match an editor palette with `themeToTreeStyles(...)`
Use `themeToTreeStyles(...)` when your app already has a VS Code or Shiki-style
theme object. It maps that theme to host styles. It also maps the theme to the
`--trees-theme-*` variables that the tree understands.
This helper gives you matching panel colors, selection colors, search-field
colors, and Git-status colors. You do not rebuild the theme system by hand.
#### Set density with `density`
Pass `density` to `useFileTree`, `preloadFileTree`, or the vanilla `FileTree`
constructor. This option sets row height and spacing together. The keyword form
(`'compact'`, `'default'`, `'relaxed'`) sets both values at once. The numeric
form keeps the default row height and sets a custom spacing factor. Every
runtime paints `--trees-item-height` and `--trees-density-override` onto the
host from the resolved density. The runtimes are vanilla CSR, vanilla SSR, React
CSR, and React SSR. This step keeps the virtualized row height and the painted
row height aligned. Caller-set inline values on the host still win. So you can
override either variable directly for a one-off. Set `itemHeight` only when you
need a row height that does not match a preset.
The keyword presets are exported as `FILE_TREE_DENSITY_PRESETS`. So SSR helpers
such as `initialVisibleRowCount` can divide by the preset row height. They do
not hard-code it.
#### Choose the right styling layer
Use:
- host styles for layout and panel framing
- CSS variables for product-specific appearance changes inside the tree
- `themeToTreeStyles(...)` when the tree must inherit an editor palette
- `getFileTreeContainer()` in vanilla when runtime code needs the mounted host
element
Add explicit overrides on top when the imported theme is close but not final.
#### `unsafeCSS` is the escape hatch
Use `unsafeCSS` for cases that the supported host and variable surfaces cannot
express. Keep it small, local, and secondary.
Do not start here. Do not rebuild the whole visual system from raw selectors.
For a broad lookup of the supported styling surfaces, read
[Styling and theming](#styling-and-theming). For icon-specific appearance
changes, continue with [Customize icons](#customize-icons).
**Style Theme CSS Variables** (`css-variables.tsx`):
```tsx
```
**Style Theme Host Styling** (`host-styling.tsx`):
```tsx
```
**Style Theme To Tree Styles** (`theme-to-tree-styles.tsx`):
```tsx
import { themeToTreeStyles } from '@pierre/trees';
const treeStyles = themeToTreeStyles(theme);
;
```
**Style Theme Unsafe CSS** (`unsafe-css.ts`):
```typescript
const fileTree = new FileTree({
paths,
unsafeCSS: `
[data-item-button][data-item-focused="true"] {
text-decoration: underline;
}
`,
});
```
### Customize icons
Start with the built-in icon sets. Then add targeted remaps only where your
product needs them. Most apps do not need to replace the whole icon system.
#### Start with the built-in icon sets
Trees includes three built-in sets:
- `minimal` for low-noise file and folder visuals
- `standard` for common language and file-type recognition
- `complete` for the broadest built-in coverage
Pass the set name directly when you need only a different baseline.
#### Adjust color mode before you remap icons
Built-in sets use semantic icon colors by default. Turn the colors off before
you use a sprite sheet. Do this when the product needs a quieter or monochrome
look.
Use the styling system for broader appearance control. Do not treat icons as a
parallel theme surface. Read
[Style and theme the tree](#style-and-theme-the-tree).
#### Use the object form for targeted remaps
Switch to `FileTreeIconConfig` when a plain set name is not enough. The
practical remap surfaces are:
- `remap` for built-in slots such as the generic file icon, chevron, dot, or
lock
- `byFileName` for exact basenames such as `package.json`
- `byFileExtension` for suffixes such as `ts` or `spec.ts`
- `byFileNameContains` for broader patterns such as `dockerfile`
This approach keeps the built-in mapping for every icon that you did not change.
#### Rule precedence matters
File-specific rules win over broader rules. Trees resolves icons in this order:
1. exact basename matches
2. basename-contains matches
3. extension matches, and the more specific suffix wins
4. the chosen built-in set
5. the generic file-slot remap or fallback
For the full lookup contract, read [Icons](#icons-resolution-order).
#### Use a sprite sheet only for advanced cases
Use `spriteSheet` in two cases. Use it when you already have branded SVG
symbols. Also use it when you need a few custom symbols next to the built-in
set.
Keep the contract simple. Provide `` definitions. Then point remap rules
at those symbols. Do not make sprite sheets the default docs path.
**Icons Basic Set** (`icons-basic.ts`):
```typescript
const fileTree = new FileTree({
paths,
icons: 'standard',
});
```
**Icons Colored Off** (`icons-colored-off.ts`):
```typescript
const fileTree = new FileTree({
paths,
icons: {
set: 'complete',
colored: false,
},
});
```
**Icons Remap** (`icons-remap.ts`):
```typescript
const fileTree = new FileTree({
paths,
icons: {
set: 'standard',
byFileName: {
'package.json': 'icon-package-json',
},
byFileExtension: {
'spec.ts': 'icon-test-file',
},
byFileNameContains: {
dockerfile: 'icon-dockerfile',
},
remap: {
'file-tree-icon-lock': 'icon-locked',
},
},
});
```
**Icons Sprite Sheet** (`icons-sprite-sheet.ts`):
```typescript
const fileTree = new FileTree({
paths,
icons: {
set: 'standard',
spriteSheet: `
`,
byFileName: {
'package.json': 'icon-package-json',
},
},
});
```
### Show Git status and row annotations
Use built-in `gitStatus` when the signal is Git-like. Use `renderRowDecoration`
when the row needs product-specific metadata that is not Git state.
#### Start with built-in `gitStatus`
`gitStatus` is the default row-signal path. It attaches statuses to canonical
paths. Folders can reflect changed descendants automatically.
Trees supports these built-in statuses:
- `added`
- `modified`
- `deleted`
- `ignored`
- `renamed`
- `untracked`
This is the shortest path when the row signal already matches Git semantics.
#### Know when `gitStatus` is enough
Use `gitStatus` only when the meaning is Git-like. Do not invent fake Git state
only to show a badge. Let Trees own the built-in status lane. Let the styling
system control how those signals look.
#### Update status sets over time
Replace the current status data directly when the surrounding app changes.
Examples are new commits, branches, comparison views, or status visibility.
This step keeps the runtime accurate. It does not promise a filesystem watcher
or a repo-sync framework.
#### Add custom row annotations with `renderRowDecoration`
Use `renderRowDecoration` when the row needs metadata that is not Git-like.
Good fits include generated-file markers, remote-storage indicators, validation
markers, and short secondary labels.
#### Choose between the two paths
Use:
- `gitStatus` when the meaning is Git-like
- `renderRowDecoration` for everything else
- both together when a row needs Git state and one extra product-specific signal
Keep annotations short and easy to read. Give a decoration accessible text or a
tooltip when it affects user decisions.
#### Keep styling separate from annotation meaning
Keep Git-status colors and decoration visuals in the same appearance system as
the rest of the tree. Use the styling and theming controls for color. Do not add
a second theme layer for annotations.
For that part of the API, read
[Style and theme the tree](#style-and-theme-the-tree) and
[Styling and theming](#styling-and-theming).
**Git Status Basic** (`git-status.ts`):
```typescript
const fileTree = new FileTree({
paths,
gitStatus: [
{ path: 'README.md', status: 'untracked' },
{ path: 'package.json', status: 'renamed' },
{ path: 'src/index.ts', status: 'modified' },
{ path: 'src/components/Button.tsx', status: 'added' },
],
});
```
**Git Status Row Decoration** (`render-row-decoration.ts`):
```typescript
const fileTree = new FileTree({
paths,
renderRowDecoration: ({ item }) => {
if (item.path.endsWith('.generated.ts')) {
return { text: 'GEN', title: 'Generated file' };
}
if (item.path.startsWith('remote/')) {
return { icon: 'icon-remote', title: 'Remote source' };
}
return null;
},
});
```
**Git Status Set** (`set-git-status.ts`):
```typescript
fileTree.setGitStatus(nextStatuses);
fileTree.setGitStatus(undefined);
```
### Handle large trees efficiently
To make large trees fast, first reduce unnecessary client work. Shape and order
the data before it reaches the UI. Then tune rendering only where the visible
window needs help.
#### Start with the main recommendation
Use:
- raw `paths` for small demos and low-ceremony cases
- `preparedInput` for larger trees
- `preparePresortedFileTreeInput(...)` when the server already owns the final
order
This order matters. Do not go straight to the rendering knobs while the client
still does avoidable shape or sort work.
#### Prepare input before it reaches the client
For larger trees, move the shape work out of the UI. Prepared input is the
scale-oriented public path.
Use `prepareFileTreeInput(...)` instead when you have only an unsorted path
list. In both cases, pass the prepared result into React, vanilla, or SSR
hydration.
#### Keep rendering work small
Trees already virtualizes the visible row window. Most apps do not need custom
virtualization primitives.
The main rendering knobs are:
- the host element CSS height, because layout sets the steady-state viewport
size
- `initialVisibleRowCount`, to budget SSR or first-render work before
measurement
- `density`, when your design changes the row density enough to matter. Pass
`'compact' | 'default' | 'relaxed'` or a custom numeric factor. This option
sets both the row height and the spacing in one place. Use `itemHeight` only
when you need a row height that does not match a preset.
- `overscan`, to trade a little extra work for smoother scrolling
Give the rendered tree host a real CSS height. The row-count hint only shapes
the first render. The browser then measures the actual viewport.
Keep the language outcome-focused. The tree mounts the visible slice and a small
buffer around it.
#### Expansion and search still affect scale
Prepared input lowers the cost to shape the data. But expansion and search still
change how many rows stay visible at once. Large expanded trees and broad search
results can widen the mounted window.
This is another reason to start with the input story first. Do not recompute the
tree shape in the client while the visible tree also changes often.
#### SSR and hydration pair naturally with prepared input
Large trees often benefit from server preload. The same scale rule applies.
Prepare or presort the input on the server one time. Preload the tree one time.
Then let the client hydrate that work. Do not recompute it.
Read [SSR](#ssr) when first paint matters. Read [SSR API](#ssr-api) when you
need the handoff contract.
#### Common pitfalls
Avoid these patterns when the tree grows:
- you send only raw `paths` for huge server-known datasets
- you re-sort on the client after the server already chose the order
- you use low-level virtualization ideas as the first fix
- you rebuild the model when `resetPaths(...)` with matching prepared input does
the job
#### What to read next
- [Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering) for
the shared input story
- [Shared concepts](#shared-concepts-scale-and-rendering-settings) for the
cross-runtime rendering knobs
- [SSR](#ssr) for server-preload-first rendering
**Large Trees Load Workspace Tree** (`load-workspace-tree.ts`):
```typescript
import { preparePresortedFileTreeInput } from '@pierre/trees';
export async function loadWorkspaceTree() {
const sortedPaths = await fetchSortedWorkspacePaths();
return preparePresortedFileTreeInput(sortedPaths);
}
```
**Large Trees Virtualization Knobs** (`virtualization-knobs.tsx`):
```tsx
const { model } = useFileTree({
preparedInput,
initialVisibleRowCount: 14,
itemHeight: 30,
overscan: 8,
});
```
### SSR
Use SSR when the tree must arrive from the server with a fast first paint. The
tree then becomes interactive on the client. SSR is not a third primary runtime.
It is a preload-and-hydrate layer over the same React or vanilla model.
#### Start with the server step
Import the preload helpers from `@pierre/trees/ssr`. Call `preloadFileTree(...)`
on the server. Treat the result as one opaque handoff object.
The rendered container still sets the steady-state height.
`initialVisibleRowCount` is only a first-render hint for SSR and hydration. The
browser then measures the real viewport.
Do not unpack the payload field by field in application code. Pass it forward
unchanged.
#### Core invariants
The server and the client must use the same tree-defining options. Match the
same input source. Match the same `id` discipline. Match the same
state-affecting options that change the first rendered tree.
The result is a hydration mismatch when the server preloads one tree and the
client builds a different one.
#### React flow
React still creates the model with `useFileTree(...)`. The one difference is
that the wrapper receives the opaque handoff object as `preloadedData`.
The model stays primary. `preloadedData` only starts hydration.
#### Vanilla flow
Vanilla uses the same preload step. But the client hydrates the server-rendered
container that is already in the page.
Render normally instead of hydrating when that server-rendered container is
missing.
#### Keep the payload opaque
In docs and application code, call the preload result an SSR payload or handoff
object. React uses it as `preloadedData`. Vanilla hydrates existing server
markup. Both flows reuse the same server work. Neither flow teaches the payload
internals as the main story.
#### Pair SSR with prepared input for large trees
Large-tree SSR works best when the server already owns the expensive shape work.
Prepare or presort the input on the server. Preload one time. Then let the
client hydrate that same result.
#### Advanced note: declarative shadow DOM
The preloaded path uses declarative shadow DOM. In React, the packaged wrapper
handles the host ownership details that it needs for hydration. Use the runtime
behavior. Do not write custom DOM diffing or raw payload plumbing.
For the API-level handoff contract, read [SSR API](#ssr-api).
**Guide Preload File Tree** (`preload-file-tree.ts`):
```typescript
import { preloadFileTree } from '@pierre/trees/ssr';
const payload = preloadFileTree({
preparedInput,
id: 'project-tree',
initialExpandedPaths: ['src'],
search: true,
initialVisibleRowCount: 11,
});
```
**Guide React Hydration** (`project-tree-client.tsx`):
```tsx
import { FileTree, useFileTree } from '@pierre/trees/react';
import type { FileTreePreparedInput } from '@pierre/trees';
import type { FileTreeSsrPayload } from '@pierre/trees/ssr';
export function ProjectTreeClient({
preparedInput,
preloadedData,
}: {
preparedInput: FileTreePreparedInput;
preloadedData: FileTreeSsrPayload;
}) {
const { model } = useFileTree({
preparedInput,
id: preloadedData.id,
initialExpandedPaths: ['src'],
search: true,
initialVisibleRowCount: 11,
});
return ;
}
```
**Guide Vanilla Hydration** (`vanilla-hydrate.ts`):
```typescript
import { FileTree } from '@pierre/trees';
const fileTree = new FileTree({
preparedInput,
id: 'project-tree',
initialExpandedPaths: ['src'],
search: true,
initialVisibleRowCount: 11,
});
const container = document.getElementById('project-tree');
if (container instanceof HTMLElement) {
fileTree.hydrate({ fileTreeContainer: container });
}
```
### Shared concepts
This page owns the cross-runtime language for Trees. React, vanilla, and SSR all
use the same path-first identity model. They use the same tree-defining inputs.
They use the same mutation vocabulary.
#### Path-first identity
Canonical path strings are the public identity for tree items. Use paths for:
- selection values
- focused-item lookups
- search matches
- rename and drag-and-drop events
- Git status attachment
- row annotations
Files and directories share the same identity space. The APIs mark the item kind
when that difference matters.
#### Input shapes
Trees accepts three related input shapes:
- `paths`: the low-ceremony path for demos, tests, and small static trees
- `preparedInput`: the recommended scale-oriented input shape
- presorted prepared input: the highest-performance prepared-input path when the
server already knows the final order
Use `prepareFileTreeInput(...)` and `preparePresortedFileTreeInput(...)` from
`@pierre/trees` to create the prepared forms. Treat prepared input as an opaque
tree-input object. Do not build the shape by hand.
#### Shared option groups
This table lists the shared meaning of the public options surface across
runtimes.
| Option | Meaning | Typical use |
| ------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `paths` | Raw canonical path list. | Small demos, tests, very small static trees. |
| `preparedInput` | Pre-shaped tree input created ahead of render. | Recommended input for larger trees. |
| `id` | Stable host identity for the tree. | Coordinate server preload and client hydration, or set a predictable DOM id. |
| `initialExpansion` | Baseline expansion policy for the first render. | Start broadly open, broadly closed, or at a specific depth. |
| `initialExpandedPaths` | Specific paths that should begin expanded. | Keep key folders open on first render. |
| `initialSelectedPaths` | Paths selected on first render. | Seed selection for previews or restored state. |
| `flattenEmptyDirectories` | Flattens chains of single-child directories into one visible row. | Compact repo-style trees. |
| `sort` | Client-side ordering for non-presorted input. | Secondary path when order must be chosen in the client. |
| `search` | Enables the built-in search surface and model search methods. | Searchable trees. |
| `initialSearchQuery` | Starting search value. | Preloaded filtered views or restored search state. |
| `fileTreeSearchMode` | Controls how matches change the visible tree. | Choose between filtering, collapsing, or expanding around matches. |
| `onSearchChange` | Callback for search-value changes. | Sync surrounding controls or analytics. |
| `onSelectionChange` | Callback for selection changes. | Update sibling UI that tracks current selection. |
| `dragAndDrop` | Enables drag and drop plus its policy hooks. | Editable trees. |
| `renaming` | Enables inline rename plus policy hooks. | Rename-in-place workflows. |
| `composition` | Header and context-menu composition surface. | Add a header row or contextual commands. |
| `gitStatus` | Built-in Git-style row signals. | Added, modified, deleted, ignored, renamed, and untracked states. |
| `icons` | Built-in icon set or icon configuration object. | Set selection, color mode, remaps, or sprite-sheet extension. |
| `renderRowDecoration` | Custom non-Git row signal renderer. | Generated-file badges, remote markers, validation hints. |
| `density` | Density preset or custom spacing factor. | Tune row height and spacing in one place. |
| `itemHeight` | Explicit row-height override. | Row height that doesn't match a `density` preset. |
| `overscan` | Extra rows rendered outside the visible window. | Smooth scrolling tradeoffs. |
| `initialVisibleRowCount` | Optional first-render row budget before the browser measures height. | Tune SSR and hydration work without pinning steady-state size. |
| `unsafeCSS` | Advanced CSS injection into the tree shadow root. | Narrow escape hatch when supported styling surfaces are not enough. |
#### Tree-shape options
The tree-shape options are `paths`, `preparedInput`, `initialExpansion`,
`initialExpandedPaths`, `flattenEmptyDirectories`, and `sort`.
Use them to answer three questions:
1. where the tree data comes from
2. what the initial visible shape must be
3. whether the client must still shape or sort the data
For the recommended setup, read
[Shape tree data for fast rendering](#shape-tree-data-for-fast-rendering).
#### Search mode semantics
Trees supports three search modes:
- `hide-non-matches`: shows only the matches and the ancestor chain that keeps
them navigable. This is the guide default.
- `collapse-non-matches`: keeps the matching paths and the ancestor chain
visible. It collapses unrelated branches out of the way.
- `expand-matches`: expands the matching branches into the surrounding tree
context. It keeps non-matching rows visible.
React and vanilla use the same search modes. Runtime pages must not redefine
them.
#### Selection, focus, and item handles
Selection is path-based. Focus is path-based. Item handles are path-oriented
helpers for a known item.
Common lookup topics are:
- get an item by path
- read the focused and selected state
- focus, select, toggle, or deselect through the model or item handle
- expand, collapse, or toggle directories through a directory handle
Read
[Navigate selection, focus, and search](#navigate-selection-focus-and-search)
for workflows. Read [React API](#react-api) for selector hooks. Read
[Vanilla API](#vanilla-api) for direct class methods.
#### Interaction and editing options
`dragAndDrop`, `renaming`, and `composition` are the runtime-agnostic editing
surfaces. They share the same path-first event model. The React and vanilla
integration points differ.
Read
[Rename, drag, and trigger item actions](#rename-drag-and-trigger-item-actions)
for the user-facing workflows that build on those options.
#### Appearance and annotation options
`gitStatus`, `icons`, `renderRowDecoration`, and `unsafeCSS` all affect how rows
look. But they do different jobs:
- `gitStatus` is the built-in Git-style signal lane
- `icons` controls icon sets, remaps, and sprite-sheet extension
- `renderRowDecoration` adds non-Git row metadata
- `unsafeCSS` is the narrow fallback when public styling surfaces are not enough
For deeper lookup, read [Styling and theming](#styling-and-theming) and
[Icons](#icons).
#### Scale and rendering settings
`initialVisibleRowCount`, `density`, `itemHeight`, and `overscan` are rendering
knobs, not onboarding concepts. The rendered container sets the steady-state
height. These options only tune the first-render budget, the visual density, and
the virtualized row window.
For density, use the `density` keyword (`'compact' | 'default' | 'relaxed'`) or
a numeric factor. It sets both the row height and the spacing in one place. The
React `` wrapper paints `--trees-item-height` and
`--trees-density-override` for you. Use `itemHeight` only when you need a row
height that does not match a preset.
Read [Handle large trees efficiently](#handle-large-trees-efficiently) when
scale becomes the real problem.
#### Mutation vocabulary
Trees exposes semantic tree mutations, not DOM events. The shared mutation terms
are:
- `add`
- `remove`
- `move`
- `batch`
- `resetPaths`
- `onMutation`
Mutation payloads stay path-first. Reset events also report whether prepared
input was involved. Invalidation metadata reports whether the canonical state or
the projected visible state changed.
Use these events to sync adjacent UI, analytics, or persistence layers. Do not
turn them into a filesystem-sync tutorial.
#### Opaque SSR payload framing
Server preload returns one handoff object. Pass it forward unchanged.
React uses that handoff as `preloadedData`. Vanilla hydrates existing server
markup through `hydrate({ fileTreeContainer })`. Neither runtime must teach the
field-by-field payload steps as the main story.
For the preload and hydration contract, read [SSR API](#ssr-api).
### React API
The React entry point lives in `@pierre/trees/react`. It is a thin React
integration layer over the same imperative model that the vanilla runtime uses.
#### React model-first story
Start with `useFileTree(options)`. Do not build a large controlled component.
The hook creates one stable model for the component lifetime. Later option
changes do not reconfigure the model. Update the model through model methods
instead.
For the shared option meanings behind that call, read
[Shared concepts](#shared-concepts-shared-option-groups).
#### `useFileTree(...)`
`useFileTree(...)`:
- accepts the shared tree-options surface
- creates the model one time
- cleans up the model when the React component unmounts
- returns `UseFileTreeResult`, which currently exposes `model`
Use model methods when the tree changes after mount. The methods include
`resetPaths(...)`, `setComposition(...)`, `setGitStatus(...)`, `setIcons(...)`,
and the search or mutation methods.
#### ``
The React component mounts the model into a host element.
Primary props:
- `model`
- normal host HTML attributes such as `className`, `style`, and `id`
React-only props:
- `header`
- `renderContextMenu`
- `preloadedData`
Behavior notes:
- it renders the model into the host element
- it hydrates instead of rendering fresh when matching preloaded content is
already present
- it uses `id ?? preloadedData?.id` for host identity during hydration
- `header` and `renderContextMenu` add React rendering onto the model
composition surface
#### Reading tree state from React
React components read from the model through selector hooks. React components
write back through model methods.
Common read patterns include:
- selected paths for sibling panels or action bars
- search state for search inputs and result counts
- custom derived snapshots such as "is this path focused?"
#### Selector hooks
`useFileTreeSelector(model, selector, equality?)`
- generic bridge from the imperative model into React
- returns a selected snapshot from the current model
- accepts an optional equality function when the selected value needs custom
comparison
`useFileTreeSelection(model)`
- convenience hook for `readonly string[]` selected paths
`useFileTreeSearch(model)`
- returns the current search snapshot and the model-backed actions
- snapshot fields: `isOpen`, `matchingPaths`, `value`
- actions: `open(initialValue?)`, `close()`, `setValue(value)`,
`focusNextMatch()`, `focusPreviousMatch()`
#### Writing to the model from React
React writes through the same imperative model methods that the vanilla runtime
exposes.
Typical write paths are:
- focus and selection methods on the model or item handles. This includes
`scrollToPath(...)`, which reveals a path without moving DOM focus and makes
optional model focus changes.
- visible-tree traversal through `focusFirstItem()`, `focusLastItem()`,
`focusNextItem()`, `focusPreviousItem()`, and `focusParentItem()`.
- visible row inspection through `getFocusedIndex()`, `getVisibleCount()`, and
`getVisibleRows(start, end)` when surrounding UI needs custom navigation.
- search methods such as `setSearch(...)` and `openSearch(...)`
- mutation methods such as `add(...)`, `remove(...)`, `move(...)`, `batch(...)`,
and `resetPaths(...)`
- runtime reconfiguration methods such as `setComposition(...)`,
`setGitStatus(...)`, and `setIcons(...)`
#### React-specific composition surface
React adds two high-level composition props on top of the model:
- `header` for React-rendered header content
- `renderContextMenu(item, context)` for React-rendered context menus
Use them when the menu or header belongs in React. The shared meaning of the
composition options is in
[Shared concepts](#shared-concepts-interaction-and-editing-options).
#### Mutation and subscriptions from React
Use selector hooks when React UI needs reactive reads. Use
`model.onMutation(...)` when you need semantic side effects. Examples are
persistence, logging, or analytics around add, remove, move, reset, or batch
operations.
#### Hydration boundary
The React runtime uses server preload through `preloadedData` on ``.
The model still comes from `useFileTree(...)`. The client must use matching
tree-defining options.
For the preload contract, read [SSR API](#ssr-api). For workflow guidance, read
[SSR](#ssr).
#### Appearance boundary
Style the React host with normal host props such as `className` and `style`. For
deeper lookup, read [Styling and theming](#styling-and-theming) and
[Icons](#icons).
**Example** (`project-tree.tsx`):
```tsx
import { FileTree, useFileTree } from '@pierre/trees/react';
export function ProjectTree({ paths }: { paths: readonly string[] }) {
const { model } = useFileTree({ paths, search: true });
return ;
}
```
**Selector Hooks** (`selector-hooks.tsx`):
```tsx
const { model } = useFileTree({ paths, search: true });
const selectedPaths = useFileTreeSelection(model);
const search = useFileTreeSearch(model);
const focusedPath = useFileTreeSelector(model, (currentModel) =>
currentModel.getFocusedPath()
);
```
### Vanilla API
The vanilla runtime lives at the package root, `@pierre/trees`.
`new FileTree(...)` is the runtime entry point and the imperative model surface.
#### Class-first story
Start with `new FileTree(options)`. Then mount the instance.
For shared option meanings, read
[Shared concepts](#shared-concepts-shared-option-groups).
#### Constructor surface
`new FileTree(options)`:
- accepts the shared options surface
- creates the model one time
- expects later updates through model methods; do not rebuild option objects for
every render
#### Mounting and lifecycle
`render(...)`
- accepts `{ fileTreeContainer }` when you already own the host element
- accepts `{ containerWrapper }` when the runtime must create and append the
host for you
`hydrate({ fileTreeContainer })`
- attaches the model to server-rendered tree markup already in the page
`unmount()`
- removes the mounted runtime and keeps the model available
`cleanUp()`
- final teardown: unmount, subscription cleanup, and model destruction
`getFileTreeContainer()`
- returns the mounted host element after render or hydrate
#### Read APIs
Common read methods are:
- `getItem(path)`
- `getFocusedItem()`
- `getFocusedPath()`
- `getFocusedIndex()`
- `getVisibleCount()`
- `getVisibleRows(start, end)`
- `getSelectedPaths()`
- `getComposition()`
- `isSearchOpen()`
- `getSearchValue()`
- `getSearchMatchingPaths()`
These reads stay path-oriented and model-oriented. They do not ask you to infer
the tree state from DOM order.
#### Item handles
`getItem(path)` returns a file handle, a directory handle, or `null`.
Shared handle actions:
- `getPath()`
- `focus()`
- `select()`
- `toggleSelect()`
- `deselect()`
Directory-only actions:
- `expand()`
- `collapse()`
- `toggle()`
#### Write and control APIs
Focus and selection control:
- `focusPath(path)`
- `focusNearestPath(path | null)`
- `focusFirstItem()` / `focusLastItem()`
- `focusNextItem()` / `focusPreviousItem()`
- `focusParentItem()`
- `scrollToPath(path, { offset: 'top' | 'center' | 'nearest', focus?: boolean })`
- `startRenaming(path?)`
- item-handle selection and focus methods
`scrollToPath(...)` scrolls the virtualizer without moving DOM focus. It updates
model focus by default. Pass `focus: false` to keep model focus unchanged.
The traversal methods follow the current visible tree, including its active
expansion and search state. `getVisibleRows(start, end)` uses inclusive row
indexes and returns path-first row metadata for building custom navigation.
Search control:
- `setSearch(value)`
- `openSearch(initialValue?)`
- `closeSearch()`
- `focusNextSearchMatch()`
- `focusPreviousSearchMatch()`
Data and mutation control:
- `add(path)`
- `remove(path, options?)`
- `move(fromPath, toPath, options?)`
- `batch(operations)`
- `resetPaths(paths, options?)`
- `onMutation(type, handler)`
Runtime reconfiguration helpers:
- `setComposition(composition?)`
- `setGitStatus(gitStatus?)`
- `setIcons(icons?)`
#### Subscriptions and external systems
Use `subscribe(listener)` when any tree change must refresh a derived snapshot.
Use `onMutation(...)` when add, remove, move, reset, or batch intent matters.
This separation keeps model subscriptions for state reads. It keeps mutation
events for semantic side effects.
#### Composition surface
The composition surface covers:
- header composition
- context-menu composition
- runtime updates through `setComposition(...)`
Read
[Rename, drag, and trigger item actions](#rename-drag-and-trigger-item-actions)
for workflow-level guidance.
#### Hydration boundary
This page names only the vanilla hydration entry point:
`hydrate({ fileTreeContainer })`. The full server-preload-first contract is in
[SSR API](#ssr-api).
#### Appearance boundary
This page names the runtime touchpoints such as `getFileTreeContainer()`,
`setGitStatus(...)`, and `setIcons(...)`. For styling variables, theme helpers,
and `unsafeCSS`, read [Styling and theming](#styling-and-theming). For icon
remaps and icon-set lookup, read [Icons](#icons).
**Example** (`vanilla-api.ts`):
```typescript
import { FileTree } from '@pierre/trees';
const fileTree = new FileTree({
paths: ['README.md', 'src/index.ts'],
search: true,
});
fileTree.render({ fileTreeContainer: container });
```
### SSR API
The SSR entry point lives in `@pierre/trees/ssr`. It owns server preload and the
server-to-client handoff contract.
#### Server-preload-first contract
Start with the server step, not the client step. Call `preloadFileTree(options)`
on the server. Then pass the returned value forward as one opaque handoff
object.
Hydration reuses that server work. It is not a separate rendering strategy with
a different public data model.
#### `preloadFileTree(...)`
`preloadFileTree(...)`:
- accepts the shared `FileTreeOptions` surface
- pre-renders the tree for first paint
- returns an opaque SSR payload
The current exported payload type name is `FileTreeSsrPayload`. But the
docs-facing rule stays the same. Pass it through unchanged.
#### `serializeFileTreeSsrPayload(...)`
`serializeFileTreeSsrPayload(payload, mode?)` turns the preloaded payload into a
full host-markup string.
Use it when your server integration needs a serialized HTML string. This avoids
a framework-specific render boundary. The payload still stays opaque at the docs
level.
#### Opaque payload framing
Treat the preload result as a handoff object. Do not treat it as a
field-by-field integration surface.
Docs can name the runtime touchpoints that use it:
- React passes the payload as `preloadedData`
- Vanilla hydrates the server-rendered tree already in the page with
`hydrate({ fileTreeContainer })`
Do not make the raw payload fields the public story.
#### Handoff rules
The server and the client must use the same tree-defining options.
Match:
- the same path source (`paths`, prepared input, or presorted prepared input)
- the same `id` discipline
- the same expansion and search options when they change the initial rendered
tree
- the same appearance options when they change the initial markup or icon output
A mismatch is not a supported partial merge. It produces a hydration mismatch or
an incorrect first state.
#### Prepared input and hydration
Hydration works with raw `paths` or prepared input. But the recommended scale
path is server-prepared input with matching client consumption.
For the shared input model behind that rule, read
[Shared concepts](#shared-concepts-input-shapes).
#### React handoff
React still creates the model with `useFileTree(...)`. Then it passes the
payload to ``.
For the full runtime surface, read [React API](#react-api). For the workflow
guide, read [SSR](#ssr).
#### Vanilla handoff
Vanilla emits the server-rendered tree into the page first. Then it creates
`new FileTree(options)` on the client. Then it calls
`fileTree.hydrate({ fileTreeContainer })` against the existing host.
For the full runtime surface, read [Vanilla API](#vanilla-api). For the workflow
guide, read [SSR](#ssr).
#### Advanced note boundary
Keep any note about declarative shadow DOM or hydration warnings short and
secondary. This page owns the public handoff contract, not the DOM internals.
**API Example** (`preload-file-tree.ts`):
```typescript
import { preloadFileTree } from '@pierre/trees/ssr';
const payload = preloadFileTree({
preparedInput,
id: 'project-tree',
initialExpandedPaths: ['src'],
initialVisibleRowCount: 11,
});
```
### Styling and theming
This page is the lookup reference for host styling, CSS-variable families,
`themeToTreeStyles(...)`, and the `unsafeCSS` escape hatch.
#### Styling entry points
Host styling comes first.
- React: use normal host `className` and `style` props on
``
- Vanilla: style the mounted host that `getFileTreeContainer()` returns
Host styles own width, height, borders, background, and panel placement. CSS
variables control the UI inside that host.
#### CSS custom property families
The tree styling surface uses variable families, not one flat list.
Core families include:
- panel, background, and foreground tokens
- hover, selection, and focus tokens
- input and search tokens
- Git-status color tokens
- icon color tokens for supported built-in icon sets
#### Fallback precedence
Trees resolves styling in this order:
1. explicit override variables such as `--trees-*-override`
2. `--trees-theme-*` variables from theme helpers
3. library defaults
So `themeToTreeStyles(...)` fills the middle layer. Direct overrides still win.
#### `themeToTreeStyles(...)`
`themeToTreeStyles(theme)` maps a VS Code or Shiki-style theme object into host
styles. It also maps the theme to the `--trees-theme-*` variables that the tree
understands.
Input shape:
- `TreeThemeInput`
- key fields: `type`, `bg`, `fg`, `colors`
Output shape:
- `TreeThemeStyles`
- compatible with React inline styles and vanilla host-style assignment
The helper covers panel colors, selection colors, focus rings, input colors, and
Git-status colors. Explicit overrides can still replace any derived token later.
#### Runtime touchpoints
React:
- keep styling on the host element through normal `className` and `style` props
- there is no separate React-only styling API beyond the shared tree options
Vanilla:
- apply styles through the host or container that you already own
- `getFileTreeContainer()` is the handoff back to application styling code
#### `unsafeCSS` escape hatch
`unsafeCSS` is the secondary path. Use it only when the supported host and
variable surfaces cannot express the customization.
Reference topics:
- the option name and public package surface
- shadow-root CSS injection at a high level
- a warning that this is the exception path, not the default styling story
Keep it narrow. Do not make it a raw stylesheet-asset strategy.
#### Related pages
- [Style and theme the tree](#style-and-theme-the-tree) for the walkthrough
- [Icons](#icons) for icon-specific configuration
- [Show Git status and row annotations](#show-git-status-and-row-annotations)
for signal meaning
**Styling Theming Reference Example** (`theme-to-tree-styles.ts`):
```typescript
import { themeToTreeStyles } from '@pierre/trees';
const treeStyles = themeToTreeStyles(theme);
```
### Icons
This page is the lookup reference for icon sets, icon remaps, sprite-sheet
extension, and icon-specific configuration rules.
#### Entry points
Built-in set selection:
- `icons: 'minimal' | 'standard' | 'complete'`
- `minimal` keeps the visual noise low
- `standard` adds common language and file-type icons
- `complete` has the broadest built-in coverage
Object configuration:
- use `FileTreeIconConfig` when a string set is not enough
- combine a base set with color toggles, slot remaps, or file-specific remaps
#### Configuration shape
Base set and color mode:
- `set?: 'minimal' | 'standard' | 'complete' | 'none'`
- `colored?: boolean`
`set: 'none'` turns off the built-in file-type mappings. File rows still fall
back to the generic file icon. Remaps can replace that icon.
Sprite-sheet extension:
- `spriteSheet?: string`
- supply an SVG string with `` definitions that remaps can target
Built-in slot remapping:
- `remap?: Record`
- common slots: chevron, generic file icon, modified-state dot, locked-path icon
File-specific remapping:
- `byFileName` for exact basename matches such as `package.json`
- `byFileNameContains` for basename substring rules such as `dockerfile`
- `byFileExtension` for extension rules without a leading dot, and for
multi-part suffixes such as `spec.ts`
#### Resolution order
File icon lookup resolves in this order:
1. exact basename match from `byFileName`
2. basename-contains match from `byFileNameContains`
3. extension match from `byFileExtension`, and the more specific suffix wins
4. built-in set mapping
5. generic file-slot remap or fallback
So `spec.ts` wins over `ts` when both rules exist.
#### `RemappedIcon` shape
`RemappedIcon` supports two forms:
- a string symbol id
- an object with `name`, and optional `width`, `height`, and `viewBox`
Use the object form when the replacement symbol needs non-default geometry
metadata.
#### Runtime touchpoints
React:
- pass the icon configuration through the model options for `useFileTree(...)`
Vanilla:
- pass the icon configuration at construction time
- update it later with `setIcons(...)`
For the runtime-specific lookup around those touchpoints, read
[React API](#react-api) and [Vanilla API](#vanilla-api).
#### Icon color interplay
Turn off the built-in colored icons with `colored: false`. The token lookup and
the fallback precedence for icon colors are in
[Styling and theming](#styling-and-theming).
#### Related pages
- [Customize icons](#customize-icons) for the walkthrough
- [Style and theme the tree](#style-and-theme-the-tree) for broader appearance
changes