Components / Structure
Tree
A hierarchical list people can open, close and select from: folders and files, a JSON or DOM tree, nested settings. It follows the WAI-ARIA tree view pattern, loads children on demand and can render hundreds of thousands of rows.
When to useLink to "When to use"
- Nested data where the hierarchy matters: a file system, a document outline, an inspector.
Use something else when:
UsageLink to "Usage"
import { Tree } from "@arshad-shah/cynosure";
<Tree label="Files" items={files} defaultExpanded={["src"]} onSelectionChange={([id]) => openFile(id)} />Selection and expansion work controlled (selected, expanded) or uncontrolled (defaultSelected, defaultExpanded).
Children on demandLink to "Children on demand"
Give an item hasChildren and no children, and pass loadChildren. The first time it opens, a loading row shows, then the children. If loading fails, a row says so and retries with Enter or a click.
Very large treesLink to "Very large trees"
With virtual, only the rows in view are in the page, so a document with 100,000 visible rows stays fast. Give the tree a height; all rows must share one height. The focused row always stays mounted, so the keyboard keeps working after scrolling.
Custom rowsLink to "Custom rows"
renderLabel replaces the row text, for a value next to a key, a badge or a count. Type-ahead still matches the plain label.
A schema explorerLink to "A schema explorer"
Groups (kind: "group") head the rows under them: Enter, Space and a click open and close them, they are never selected, and they end with their child count. Items take an icon colour (iconTone, iconColor), a trailing count or badge, your own data (handed back in every callback), actions (buttons at the row's end) and a contextMenu. A pressed action or a chosen command calls onCommand(value, item).
<Tree<Table>
label="Schema"
items={[{ id: "tables", label: "Tables", kind: "group", children: tables.map((t) => ({
id: t.name, label: t.name, icon: table, iconTone: "accent", trailing: format(t.rows), data: t,
actions: [{ value: "view", label: "View data", icon: rows }],
contextMenu: [{ value: "view", label: "View data" }, { value: "copy", label: "Copy name" }],
})) }]}
onCommand={(command, item) => run(command, item.data)}
/>Actions show while their row is hovered, focused or selected, and always on screens without hover. On the focused row they come right after it in the Tab order: Tab reaches them, Shift+Tab or Escape goes back to the row, and the arrows still move between rows. The tree keys stay those of the APG pattern and the tree keeps a single way in; every action can also be in the context menu, which Shift+F10 opens.
FilteringLink to "Filtering"
filter shows only matching items, with their ancestors opened and their own children, and marks the matched text with HighlightedText. A string matches anywhere in the label, ignoring case. A function returns true, false, or the positions a fuzzy matcher found, which are marked exactly. The expanded ids are not changed, and an opened ancestor can still be closed. Only known children are searched.
Loading from a storeLink to "Loading from a store"
When children are fetched somewhere else (a store), put them in children when they arrive and say what is happening with loadingIds and errorIds. onRetry is called when a failed item is retried; renderError(item, retry) and renderEmpty(item) draw the error row and the row under an open item with no children. With loadChildren, the handle's reload(id) loads an item's children again.
PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
labelRequired | Accessible name of the tree. | string | |
itemsRequired | The top-level items; each can hold more under children. | TreeItem<T>[] | |
selected | Selected ids (controlled) and the default for uncontrolled use. | string[] | |
defaultSelected | Ids selected at the start when uncontrolled. | string[] | |
onSelectionChange | Called with the selected ids when the selection changes. | ((ids: string[]) => void) | |
expanded | Expanded ids (controlled) and the default for uncontrolled use. | string[] | |
defaultExpanded | Ids expanded at the start when uncontrolled. | string[] | |
onExpandedChange | Called with the expanded ids when an item is expanded or collapsed. | ((ids: string[]) => void) | |
selectionMode | single (default): one at a time. multiple: Space and clicks toggle. | "single" | "multiple" | |
loadChildren | Fetch the children of an item that hasChildren but has none yet. | ((item: TreeItem<T>) => Promise<TreeItem<T>[]>) | |
loadingIds | Items whose children are loading somewhere else (a store): a loading row shows under them while open and they have no children yet; with children, the item is only marked busy. | readonly string[] | |
errorIds | Items whose children failed to load somewhere else: an error row shows under them in place of any children. | readonly string[] | |
onRetry | Called when a failed item is retried (Enter or a click on its error row, or renderError's retry). | ((item: TreeItem<T>) => void) | |
renderError | Content of the error row of an item whose children failed. retry runs loadChildren again and onRetry. | ((item: TreeItem<T>, retry: () => void) => ReactNode) | |
renderEmpty | Content of a row shown under an open item that has no children ("No columns"). Without it, nothing shows. | ((item: TreeItem<T>) => ReactNode) | |
filter | Show only matching items, with their ancestors (opened) and their own children, and mark the matched text. A string matches the label anywhere, ignoring case. Only known children are searched. Pass a stable function (useCallback): branches closed while filtering open again when it changes. | TreeFilter<T> | |
renderLabel | Custom row content after the chevron and icon (a value, a badge, a line number). label is the label as the tree would draw it (with matches marked while filtering). The item's label still drives type-ahead. | ((item: TreeItem<T>, label: ReactNode) => ReactNode) | |
onAction | Enter (or a double-click) on an item: its default action, such as opening it or copying its path. Space selects. | ((item: TreeItem<T>) => void) | |
onCommand | Called with the value of a pressed row action or a chosen context menu command, and the row's item. | ((value: string, item: TreeItem<T>) => void) | |
handle | Imperative control: reload(id). | Ref<TreeHandle> | |
virtual | Render only the rows in view (for tens of thousands of rows). Give the tree a height (style or a class); rows must share one height. | boolean | |
size | Row height and indent. Default "md". | "sm" | "md" | "md" |
labels | Replace the built-in English text: the loading row, the failed-to-load row and the context menu's name. | Partial<TreeLabels> | |
className | Added to the root element, for layout. | string | |
style | Layout only. Restyle through --cy-tree-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
| Input | Result |
|---|---|
| Down arrow / Up arrow | Next / previous visible row (disabled and loading rows are skipped) |
| Right arrow | Opens a closed branch; on an open one, moves to its first child |
| Left arrow | Closes an open branch; otherwise moves to the parent |
| Home / End | First / last visible row |
| Enter | Selects the row (only that row), or runs onAction; on a group, opens or closes; on a failure row, retries |
| Space | Selects; with selectionMode="multiple", toggles; on a group, opens or closes |
| Tab | From the focused row, to its action buttons; then out of the tree |
| Escape | From an action button, back to its row |
| Shift+F10, context menu key | Opens the row's context menu |
| * | Opens every branch at the same level |
| Letters | Jump to the next row whose label starts with them |
| Click / chevron | Selects / opens or closes without selecting |
- Roles:
role="tree"(witharia-multiselectablein multiple mode) andtreeitemrows witharia-level,aria-posinset,aria-setsize,aria-expanded,aria-selected(not on groups),aria-busywhile children load andaria-keyshortcuts="Shift+F10"with a context menu. A row with actions is named by its label and trailing text. - Context menu: one ContextMenu beside the tree, not inside it (a tree holds only tree items); focus returns to the row.
- Focus: one Tab stop; the focused row is the only row in the Tab order, followed by its action buttons.
- Direction: in right-to-left text, Left and Right swap, and closed chevrons point left.
- Performance: only expanded branches are rendered, and rows are memoised, so moving focus re-renders two rows even in a tree of thousands.
TouchLink to "Touch"
Rows are at least 44px tall on coarse pointers. Row actions always show on screens without hover, with 44px hit areas, and a long press opens a row's context menu.
ThemingLink to "Theming"
Tokens: --cy-tree-indent, --cy-tree-row-height, --cy-tree-selected-bg, --cy-tree-group-ink (group rows) and --cy-tree-trailing-ink (trailing text; on a selected row it takes the row's colour).
createTheme({ components: { tree: { indent: "20px", rowHeight: "28px" } } });RelatedLink to "Related"
HighlightedText · ContextMenu · Accordion · Menu · Combobox
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.