Overview

Trees is in beta. Start from the public React, vanilla, and SSR entry points on this page. Expect polish and small API shifts between beta releases.

Trees keeps one path-first model across React, vanilla, and SSR hydration. Selection, focus, search, rename, drag and drop, Git status, and row annotations all work in terms of canonical paths.

These docs stay guide-first. Pick your runtime, shape tree data before it reaches the UI, and then add search, item actions, styling, icons, row signals, or SSR as needed.

Guides

@pierre/trees exposes one path-first model and two primary runtime entries: a thin React layer in @pierre/trees/react and the vanilla class in @pierre/trees.

Choose your integration

1. Path-first identity

Treat canonical path strings as the public identity for every item in the tree. For example, src/components/Button.tsx is not just a label on screen. It is the value you read from selection state, the path you focus programmatically, and the target you rename or move later.

For the shared vocabulary behind that rule, jump to Shared concepts.

2. React vs Vanilla JS

Pick the React entry point when the surrounding UI already lives in React. Jump to the getting started with React guide for more info.

Pick the vanilla class when your app is not React-based, or when another framework will own lifecycle around an imperative model. Jump to the getting started with vanilla JS for more.

3. Understand tree-shape

Both runtimes consume the same tree data. Small examples can start with raw paths, but real application trees should move to prepared input before the client pays that shaping work on every load. Regardless of your runtime being React or vanilla JS, be sure to read Shape tree data for fast rendering after your runtime quickstart.

Get started with React

Use the React entry point when your surrounding UI already lives in React. The hook creates one stable tree model, and the component mounts that model into the host element.

Install @pierre/trees

Use the package root for the vanilla runtime, and the /react entry point for the React wrapper.

Create the model with useFileTree(...)

useFileTree(...) from @pierre/trees/react creates the model exactly once for the component lifetime. Later option changes are not a controlled update path. If the tree data or runtime behavior changes after mount, update the model through methods such as resetPaths(...), setComposition(...), setGitStatus(...), or setIcons(...).

For small trees, you can pass raw paths. For scalable trees, prefer preparedInput produced on the server or another non-UI boundary.

If you are still deciding how to shape that input, read Shape tree data for fast rendering after this quickstart.

Render with <FileTree model={model} />

<FileTree /> is a thin React wrapper over the model. It mounts the tree into the host element, forwards normal host props such as className and style, and hydrates existing server output when you pass preloadedData later.

Keep the mental model simple: the model owns the tree state, and the React component renders it.

Read and update tree state through the model

React code reads snapshots from the model through selector hooks, and writes back through model methods.

Use useFileTreeSelector(model, selector, equality?) when sibling UI needs a custom derived snapshot without rerendering on unrelated tree changes. For the shared interaction vocabulary, read Navigate selection, focus, and search and 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. Once the tree becomes expensive to shape or sort on the client, move that work out of the UI.

Move to prepared input before the tree gets expensive

The recommended scale path is:

  1. Load canonical paths on the server or another non-UI boundary.
  2. Prepare the tree input once.
  3. Pass preparedInput into useFileTree(...).

If the server already knows the final order, preparePresortedFileTreeInput(...) is the highest-performance prepared-input variant because the client can skip both shaping and extra sorting work.

Add SSR later when first paint matters

Hydration layers on top of the same model-first React story. The client still calls useFileTree(...), and the React wrapper still renders the same model. The only difference is that the tree starts from preloaded server output.

When you need that flow, continue with SSR and SSR API.

Get started with vanilla

Use the vanilla runtime when your app is not React-based, or when another framework will own lifecycle around an imperative tree model. new FileTree(...) creates the model, and render(...) or hydrate(...) attaches it to the DOM.

Install @pierre/trees

Create the model with new FileTree(...)

The class instance is both the runtime entry point and the state surface. For small trees, you can pass raw paths. For scalable trees, prefer preparedInput produced outside the UI.

Render into a host element

render({ fileTreeContainer }) mounts the model into an existing host element. If you want the runtime to create the host for you, use render({ containerWrapper }) instead.

Keep the boundary clear: the model owns tree state, and the mounted host only renders that state. Do not scrape the DOM to find the selected item or 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.

When surrounding data changes, call explicit model methods such as resetPaths(...), setComposition(...), setGitStatus(...), or setIcons(...) instead of reconstructing the model 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

The recommended scale path is:

  1. Load canonical paths outside the UI.
  2. Prepare the tree input once.
  3. Construct new FileTree({ preparedInput, ... }).

If the server or indexer already knows the final order, preparePresortedFileTreeInput(...) is the better fit because the client can skip extra sorting work.

Add SSR later when server rendering matters

Hydration layers on top of the same class-first runtime. The client still creates new FileTree(...), but instead of rendering fresh markup it attaches that model to server-rendered tree output with hydrate({ fileTreeContainer }).

When you need that flow, continue with SSR and SSR API.

Advanced note: wrapping the vanilla model in another framework

If you wrap the vanilla model in another framework, keep the ownership boundary the same:

  • 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 itself

That keeps React as the only first-class wrapper surface without changing how the underlying model works.

Shape tree data for fast rendering

Shape the tree before it reaches the UI when the dataset is large enough for client-side preparation to matter. Trees treats prepared input as a first-class public concept, not as an internal optimization trick.

Start with server-prepared input for scalable trees

prepareFileTreeInput(...) lets the client skip repeated tree-shaping work. The best place to do that preparation is usually the server, a loader, or another non-UI boundary that already has the full path list.

The client still works with the same path-first model. Only the expensive preparation step moves earlier.

Pass preparedInput into the runtime

Both primary runtimes consume the same prepared payload shape.

Use simple paths input only for small trees

Raw paths is still the right call for small demos, tests, and very small static trees.

That is the easy starting path, not the scale-oriented default. When the tree grows, move the preparation work out of the client.

Presorted input is the highest-performance prepared-input path

If your server or indexer already knows the final order, use preparePresortedFileTreeInput(...). That skips both the tree-shaping work and the extra ordering work a normal prepared input pass would still apply.

Use this when the backend, build step, or cached index already owns the sort order. Do not reimplement default ordering manually in the client just to reach the presorted path.

Prepare on the server, render on the client

The recommended split is simple:

  1. Load canonical paths outside the UI.
  2. Prepare the input once.
  3. Pass preparedInput into React, vanilla, or SSR hydration.

That reduces client CPU work, makes startup cost more predictable, and lets the same prepared payload feed every runtime.

Keep client-side sorting and preparation secondary

Sometimes data only exists in the browser, or custom ordering must run there. Trees supports that, but it is the exception path. Start with prepared input when the app can move work out of the UI, and treat client-side shaping as a fallback.

For the shared contract behind these inputs, see Shared concepts. For the scale-oriented version of the same guidance, continue with Handle large trees efficiently.

The tree model tracks three user-facing states that matter immediately: selection, focus, and the visible row set. Search layers on top of that same model instead of introducing a separate identity system.

Selection, focus, keyboard movement, and search all work in terms of canonical paths. If the visible tree changes because a branch collapses or search filters rows away, your app still reasons about the same path values.

That is why the public APIs return readonly string[] for selected paths, path strings for focused items, and path strings for search matches.

Focus tells you where keyboard actions land. Selection tells you which rows the app currently treats as chosen. They often move together, but they are not interchangeable.

If you care about multi-select state in surrounding UI, read the selected paths. If you care about where rename, keyboard navigation, or a command will land next, read the focused path or item handle.

Keyboard movement works on the currently visible, expanded tree. Expanding or collapsing a branch changes which row can receive focus next. Search changes the same visible tree, so search also changes where keyboard movement can land.

That behavior is why Trees exposes focus and selection through the model instead of through DOM order or row indexes.

Search matches the same path-first tree data. It changes what stays visible, but it does not invent a second identity model. Selection and focus still refer to paths, even while the visible window changes around the current query.

Start with hide-non-matches unless you have a stronger product reason to preserve unrelated branches on screen. It aligns with the common "find the thing I want" workflow, and 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 lives in Shared concepts.

React reads from the model through selector hooks and writes back through model methods.

Use useFileTreeSelector(...) when sibling UI needs a custom derived snapshot.

Vanilla code wires the same behavior around the class instance.

The DOM host is not the source of truth. The model is.

Rename, drag, and trigger item actions

This guide covers the main row-level editing workflows: rename first, drag and drop second, and optional command surfaces such as context menus after that. 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

Teach those flows before you reach for a generic mutation inventory. Users care about which path moved, which path was renamed, and which row stays focused next.

Rename items in place

Enable renaming when users should be able to rename rows inline. The common policy hooks are:

  • canRename(item) to block protected files or folders
  • onRename(event) to react to the final path change
  • onError(error) to surface invalid rename attempts

The rename event reports the source path, the destination path, and whether the renamed item was a folder.

Move items with drag and drop

Enable dragAndDrop when users should 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 dragged paths plus the resolved target shape. They do not ask you to reverse-engineer DOM rows.

Combine rename and drag safely

A common editable project tree enables both renaming and dragAndDrop, then 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/
  • surface rename and drop failures outside the tree instead of failing 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 without requiring the menu.

In React, the wrapper can render menu content for you:

In vanilla, use composition.contextMenu.render when you want to provide the menu element directly from the runtime config.

Keep keyboard and focus behavior predictable

The focused row is the anchor for rename and keyboard-invoked commands. Opening and closing a menu should restore focus predictably, and keyboard users should still be able to reach the main actions without relying on 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 persist those changes to a server, local state, or another boundary. Keep that separation clear.

If you need the shared vocabulary behind those events, read Shared concepts. For runtime lookup, jump to React API or Vanilla API.

Style and theme the tree

Start with the host element, then move inward. Host styles control the outer panel. CSS variables control most tree-internal appearance. themeToTreeStyles(...) maps an editor-like theme into the same variable system. unsafeCSS is the escape hatch when the supported surfaces do not 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 <FileTree model={...} />.

In vanilla, style the mounted host element you already own. After mount, getFileTreeContainer() gives you that element back when runtime code needs to update it.

Use CSS variables for most visual changes

CSS variables are the main public styling surface inside the shadow root. Reach for them before custom CSS injection.

That approach keeps the normal fallback chain intact:

  1. explicit override variables
  2. --trees-theme-* variables from theme helpers
  3. library defaults

For the token families behind those variables, see 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 plus the --trees-theme-* variables the tree already understands.

That gives you matching panel colors, selection colors, search-field colors, and Git-status colors without rebuilding the theme system by hand.

Set density with density

Pass density to useFileTree (or preloadFileTree, or the vanilla FileTree constructor) to bundle row height and spacing into one option. The keyword form ('compact', 'default', 'relaxed') resolves both at once; a numeric form keeps the default row height and supplies a custom spacing factor. Every runtime — vanilla CSR, vanilla SSR, React CSR, and React SSR — paints --trees-item-height and --trees-density-override onto the host from the resolved density so the virtualized and painted row heights stay aligned. Caller-set inline values on the host still win, so you can override either variable directly when you need a one-off. Set itemHeight only when you need a row height that doesn't match a preset.

The keyword presets are exported as FILE_TREE_DENSITY_PRESETS so SSR helpers like initialVisibleRowCount can divide by the preset's row height without hard-coding 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 should inherit an editor palette
  • getFileTreeContainer() in vanilla when runtime code needs the mounted host element

Layer explicit overrides on top when the imported theme is close but not final.

unsafeCSS is the escape hatch

unsafeCSS is for the cases the supported host and variable surfaces cannot express. Keep it small, local, and secondary.

Do not start here, and do not rebuild the whole visual system from raw selectors. If you need a broad lookup of the supported styling surfaces, read Styling and theming. For icon-specific appearance changes, continue with Customize icons.

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 ships 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

If you only need a different baseline, pass the set name directly.

Adjust color mode before you remap icons

Built-in sets default to semantic icon colors. Turn that off before you reach for a sprite sheet if the product wants a quieter or monochrome look.

For broader appearance control, use the styling system instead of treating icons as a parallel theme surface. See 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

That keeps the built-in mapping for everything you did not customize.

Rule precedence matters

File-specific rules beat broader ones. In practice, Trees resolves icons in this order:

  1. exact basename matches
  2. basename-contains matches
  3. extension matches, with more specific suffixes winning
  4. the chosen built-in set
  5. the generic file-slot remap or fallback

If you want the full lookup contract, read Icons.

Use a sprite sheet only for advanced cases

spriteSheet is the path when you already have branded SVG symbols or need a few custom symbols that should coexist with the built-in set.

Keep the contract simple: provide <symbol> definitions, then reference those symbols through remap rules. Do not turn sprite sheets into the default docs path.

Show Git status and row annotations

Use built-in gitStatus when the signal is Git-like. Reach for 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, and folders can reflect changed descendants automatically.

Trees supports these built-in statuses:

  • added
  • modified
  • deleted
  • ignored
  • renamed
  • untracked

That is the shortest path when the row signal already matches Git semantics.

Know when gitStatus is enough

Use gitStatus when the meaning is actually Git-like. Do not invent fake Git state just to get a badge on screen. Let Trees own the built-in status lane, and let the styling system control how those signals look.

Update status sets over time

When the surrounding app changes commits, branches, comparison views, or status visibility, replace the current status data explicitly.

That keeps the runtime truthful without promising 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, or 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 plus one extra product-specific signal

Keep annotations short and glanceable. If a decoration affects user decisions, give it accessible text or a tooltip.

Keep styling separate from annotation meaning

Git-status colors and decoration visuals should stay in the same appearance system as the rest of the tree. Use styling and theming controls for color, not a second annotation-specific theme layer.

For that part of the API, see Style and theme the tree and Styling and theming.

Handle large trees efficiently

Large-tree performance starts by reducing unnecessary client work. Shape and order the data before it reaches the UI, then tune rendering only where the visible window actually 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

That order matters. Do not jump straight to rendering knobs if the client is still doing avoidable shaping or sorting work.

Prepare input before it reaches the client

For larger trees, move shaping work out of the UI. Prepared input is the scale-oriented public path.

If you only have an unsorted path list, use prepareFileTreeInput(...) instead. Either way, 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's CSS height, because steady-state viewport size comes from layout
  • initialVisibleRowCount, when you want to budget SSR or first-render work before measurement
  • density, when your design changes row density enough to matter — pass 'compact' | 'default' | 'relaxed' (or a custom numeric factor) to resolve both the row height and the surrounding spacing in one place. Use itemHeight only when you need a row height that doesn't match a preset.
  • overscan, when you need 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 before the browser can measure the actual viewport.

Keep the language outcome-focused: the tree mounts the visible slice plus a small buffer around it.

Expansion and search still affect scale

Prepared input lowers the cost of shaping 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.

That is another reason to start with the input story first. If the visible tree changes often, avoid recomputing the underlying shape in the client at the same time.

SSR and hydration pair naturally with prepared input

Large trees often benefit from server preload. The same scale rule still applies: prepare or presort the input on the server once, preload the tree once, and let the client hydrate that work instead of recomputing it.

Read SSR when first paint matters, and SSR API when you need the handoff contract.

Common pitfalls

Avoid these patterns when the tree grows:

  • sending only raw paths for huge server-known datasets
  • re-sorting on the client after the server already chose the order
  • treating low-level virtualization ideas as the first fix
  • rebuilding the model when resetPaths(...) with matching prepared input would do the job

SSR

Use SSR when you want the tree to arrive from the server with a fast first paint, then become interactive on the client. This 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 preload helpers from @pierre/trees/ssr, call preloadFileTree(...) on the server, and treat the result as one opaque handoff object.

Steady-state height still comes from the rendered container. initialVisibleRowCount is only a first-render hint for SSR and hydration before the browser can measure the real viewport.

Do not unpack the payload into a field-by-field integration story in application code. Pass it forward unchanged.

Core invariants

Server and client must agree on the same tree-defining options. Match the same input source, the same id discipline, and the same state-affecting options that change the first rendered tree.

If the server preloads one tree and the client constructs a different one, hydration mismatch is the expected outcome.

React flow

React still creates the model with useFileTree(...). The difference is that the wrapper receives the opaque handoff object as preloadedData.

The model stays primary. preloadedData only activates hydration.

Vanilla flow

Vanilla uses the same preload step, but the client hydrates the server-rendered container that is already in the page.

If that server-rendered container is missing, render normally instead of hydrating.

Keep the payload opaque

In docs and application code, refer to the preload result as an SSR payload or handoff object. React consumes it as preloadedData. Vanilla hydrates existing server markup. Both flows reuse the same server work without teaching 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 shaping work. Prepare or presort the input on the server, preload once, then let the client hydrate that same result.

Advanced note: declarative shadow DOM

The preloaded path uses declarative shadow DOM under the hood. In React, the packaged wrapper already handles the host ownership details it needs for hydration. Use the runtime behavior instead of inventing custom DOM diffing or raw payload plumbing.

For the API-level handoff contract, read SSR API.

Reference

Shared concepts

This page owns the cross-runtime language for Trees. React, vanilla, and SSR all use the same path-first identity model, the same tree-defining inputs, and the same mutation vocabulary.

Path-first identity

Canonical path strings are the public identity for tree items. Use paths everywhere:

  • 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. APIs distinguish 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, not as a hand-rolled shape.

Shared option groups

This table covers the shared meaning of the public options surface across runtimes.

OptionMeaningTypical use
pathsRaw canonical path list.Small demos, tests, very small static trees.
preparedInputPre-shaped tree input created ahead of render.Recommended input for larger trees.
idStable host identity for the tree.Coordinate server preload and client hydration, or set a predictable DOM id.
initialExpansionBaseline expansion policy for the first render.Start broadly open, broadly closed, or at a specific depth.
initialExpandedPathsSpecific paths that should begin expanded.Keep key folders open on first render.
initialSelectedPathsPaths selected on first render.Seed selection for previews or restored state.
flattenEmptyDirectoriesFlattens chains of single-child directories into one visible row.Compact repo-style trees.
sortClient-side ordering for non-presorted input.Secondary path when order must be chosen in the client.
searchEnables the built-in search surface and model search methods.Searchable trees.
initialSearchQueryStarting search value.Preloaded filtered views or restored search state.
fileTreeSearchModeControls how matches change the visible tree.Choose between filtering, collapsing, or expanding around matches.
onSearchChangeCallback for search-value changes.Sync surrounding controls or analytics.
onSelectionChangeCallback for selection changes.Update sibling UI that tracks current selection.
dragAndDropEnables drag and drop plus its policy hooks.Editable trees.
renamingEnables inline rename plus policy hooks.Rename-in-place workflows.
compositionHeader and context-menu composition surface.Add a header row or contextual commands.
gitStatusBuilt-in Git-style row signals.Added, modified, deleted, ignored, renamed, and untracked states.
iconsBuilt-in icon set or icon configuration object.Set selection, color mode, remaps, or sprite-sheet extension.
renderRowDecorationCustom non-Git row signal renderer.Generated-file badges, remote markers, validation hints.
densityDensity preset or custom spacing factor.Tune row height and spacing in one place.
itemHeightExplicit row-height override.Row height that doesn't match a density preset.
overscanExtra rows rendered outside the visible window.Smooth scrolling tradeoffs.
initialVisibleRowCountOptional first-render row budget before the browser measures height.Tune SSR and hydration work without pinning steady-state size.
unsafeCSSAdvanced 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 should be
  3. whether the client still needs to shape or sort the data

For the recommended setup, read Shape tree data for fast rendering.

Search mode semantics

Trees supports three search modes:

  • hide-non-matches: filters visible rows down to matches plus the ancestor chain needed to keep them navigable. This is the guide default.
  • collapse-non-matches: keeps matching paths and their ancestor chain visible, but collapses unrelated branches out of the way.
  • expand-matches: expands matching branches into surrounding tree context and keeps non-matching rows visible.

React and vanilla use the same search modes. Runtime pages should 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 focused and selected state
  • focus, select, toggle, or deselect through the model or item handle
  • expand, collapse, or toggle directories through a directory handle

Use Navigate selection, focus, and search for workflows, React API for selector hooks, and 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 even though the React and vanilla integration points differ.

Use Rename, drag, and trigger item actions for the user-facing workflows built on top of 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, jump to Styling and theming and Icons.

Scale and rendering settings

initialVisibleRowCount, density, itemHeight, and overscan are rendering knobs, not onboarding concepts. The rendered container sets steady-state height; these options only tune the first-render budget, visual density, and the virtualized row window.

For density, prefer the density keyword ('compact' | 'default' | 'relaxed') or a numeric factor — it resolves both row height and spacing in one place, and the React <FileTree> wrapper paints --trees-item-height and --trees-density-override for you. Reach for itemHeight only when you need a row height that doesn't match a preset.

Use Handle large trees efficiently when scale becomes the real problem.

Mutation vocabulary

Trees exposes semantic tree mutations rather than DOM events. The shared mutation terms are:

  • add
  • remove
  • move
  • batch
  • resetPaths
  • onMutation

Mutation payloads stay path-first. Reset events also tell you whether prepared input was involved, and invalidation metadata tells you whether canonical state or 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 consumes that handoff as preloadedData. Vanilla hydrates existing server markup through hydrate({ fileTreeContainer }). Neither runtime should teach field-by-field payload choreography as the main story.

For the preload and hydration contract, read 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 the vanilla runtime uses.

React model-first story

Start with useFileTree(options), not with a giant controlled component. The hook creates one stable model for the component lifetime. Later option changes are not a controlled reconfiguration path. Update the model through model methods instead.

For the shared option meanings behind that call, read Shared concepts.

useFileTree(...)

useFileTree(...):

  • accepts the shared tree options surface
  • creates the model exactly once
  • cleans the model up when the React component unmounts
  • returns UseFileTreeResult, which currently exposes model

Keep using model methods such as resetPaths(...), setComposition(...), setGitStatus(...), setIcons(...), and the search or mutation methods when the tree changes after mount.

<FileTree />

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 when hydration is involved
  • header and renderContextMenu layer React rendering onto the model's composition surface

Reading tree state from React

React components read from the model through selector hooks and 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 plus 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 the vanilla runtime exposes.

Typical write paths are:

  • focus and selection methods on the model or item handles
  • 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 underlying composition options still lives in Shared concepts.

Mutation and subscriptions from React

Use selector hooks when React UI needs reactive reads. Use model.onMutation(...) when you need semantic side effects such as persistence, logging, or analytics around add, remove, move, reset, or batch operations.

Hydration boundary

The React runtime consumes server preload through preloadedData on <FileTree />. The model still comes from useFileTree(...), and the client must use matching tree-defining options.

For the preload contract, read SSR API. For workflow guidance, read SSR.

Appearance boundary

Style the React host with normal host props such as className and style. For deeper lookup, use Styling and theming and Icons.

Vanilla API

The vanilla runtime lives at the package root, @pierre/trees. new FileTree(...) is both 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.

Constructor surface

new FileTree(options):

  • accepts the shared options surface
  • establishes the model once
  • expects later updates through model methods instead of rebuilding option objects for every render

Mounting and lifecycle

render(...)

  • accepts { fileTreeContainer } when you already own the host element
  • accepts { containerWrapper } when the runtime should 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 while keeping the model available

cleanUp()

  • final teardown: unmount plus 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()
  • getSelectedPaths()
  • getComposition()
  • isSearchOpen()
  • getSearchValue()
  • getSearchMatchingPaths()

These reads stay path- and model-oriented. They do not ask you to infer tree state from DOM order.

Item handles

getItem(path) returns either 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)
  • startRenaming(path?)
  • item-handle selection and focus methods

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 should refresh a derived snapshot. Use onMutation(...) when add, remove, move, reset, or batch intent matters.

That separation keeps model subscriptions for state reads and mutation events for semantic side effects.

Composition surface

The composition surface covers:

  • header composition
  • context-menu composition
  • runtime updates through setComposition(...)

Use Rename, drag, and trigger item actions for workflow-level guidance.

Hydration boundary

This page only names the vanilla hydration entry point: hydrate({ fileTreeContainer }). The full server-preload-first contract lives in 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. For icon remaps and icon-set lookup, read Icons.

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 back into a full host-markup string.

Use it when your server integration needs a serialized HTML string instead of passing the payload through 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, not as a field-by-field integration surface.

Docs can name the runtime touchpoints that consume it:

  • React passes the payload as preloadedData
  • Vanilla hydrates the server-rendered tree already in the page with hydrate({ fileTreeContainer })

Do not center raw payload fields as the public story.

Handoff rules

Server and client must agree on 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-affecting options when they change the initial rendered tree
  • the same appearance-affecting options when they change initial markup or icon output

Mismatches are not a supported partial merge. They produce hydration mismatch or 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.

React handoff

React still creates the model with useFileTree(...), then passes the payload to <FileTree model={model} preloadedData={payload} />.

For the full runtime surface, read React API. For the workflow guide, read SSR.

Vanilla handoff

Vanilla emits the server-rendered tree into the page first, creates new FileTree(options) on the client, then calls fileTree.hydrate({ fileTreeContainer }) against the existing host.

For the full runtime surface, read Vanilla API. For the workflow guide, read SSR.

Advanced note boundary

If you mention declarative shadow DOM or hydration warnings, keep that note short and secondary. This page owns the public handoff contract, not DOM internals.

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 <FileTree model={...} />
  • Vanilla: style the mounted host returned by getFileTreeContainer()

Host styles own width, height, borders, background, and panel placement. CSS variables control the UI rendered inside that host.

CSS custom property families

The tree styling surface is organized around variable families rather than 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 supplied by theme helpers
  3. library defaults

That means themeToTreeStyles(...) fills the middle layer, and direct overrides still win.

themeToTreeStyles(...)

themeToTreeStyles(theme) maps a VS Code or Shiki-style theme object into host styles plus the --trees-theme-* variables the tree already 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:

  • styling stays on the host element through normal className and style props
  • there is no separate React-only styling API beyond shared tree options

Vanilla:

  • apply styles through the host or container 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 needed customization.

Reference topics:

  • option name and public package surface
  • shadow-root CSS injection at a high level
  • warning that this is the exception path, not the default styling story

Keep it narrow. Do not turn it into a raw stylesheet-asset strategy.

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' disables built-in file-type mappings. File rows still fall back to the generic file icon unless remaps replace it.

Sprite-sheet extension:

  • spriteSheet?: string
  • supply an SVG string with <symbol> definitions that remaps can target

Built-in slot remapping:

  • remap?: Record<string, RemappedIcon>
  • 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, including 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, with more specific suffixes winning
  4. built-in set mapping
  5. generic file-slot remap or fallback

That means spec.ts should beat ts when both rules exist.

RemappedIcon shape

RemappedIcon supports two forms:

  • a string symbol id
  • an object with name, plus optional width, height, and viewBox

Use the object form when the replacement symbol needs non-default geometry metadata.

Runtime touchpoints

React:

  • pass icon configuration through the model options used by useFileTree(...)

Vanilla:

  • pass icon configuration at construction time
  • update it later with setIcons(...)

For the runtime-specific lookup around those touchpoints, read React API and Vanilla API.

Icon color interplay

Built-in colored icons can be disabled with colored: false. The actual token lookup and fallback precedence for icon colors live in Styling and theming.