All components subtlesolidxssmmd
Tree View
Nested hierarchies with expandable branches, indent guides, and depth-aware padding.
src
components
card.tsx
package.json
README.md
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859import { TreeView } from "#components/ui/ark-ui/tree-view.tsx";import { createTreeCollection } from "@ark-ui/react/tree-view";
const collection = createTreeCollection({ rootNode: { value: "ROOT", label: "", children: [ { value: "src", label: "src", children: [{ value: "index.ts", label: "index.ts" }], }, ], },});
function TreeNode({ node, indexPath }) { return ( <TreeView.NodeProvider node={node} indexPath={indexPath}> {node.children ? ( <TreeView.Branch> {/* indicator rotation and selected styling resolve from the machine's node state */} <TreeView.BranchControl> <TreeView.BranchIndicator> <TreeView.Icon viewBox="0 0 16 16">{/* chevron */}</TreeView.Icon> </TreeView.BranchIndicator> <TreeView.BranchText>{node.label}</TreeView.BranchText> </TreeView.BranchControl> <TreeView.BranchContent> <TreeView.BranchIndentGuide /> {node.children.map((child, index) => ( <TreeNode key={child.value} node={child} indexPath={[...indexPath, index]} /> ))} </TreeView.BranchContent> </TreeView.Branch> ) : ( <TreeView.Item> <TreeView.ItemText>{node.label}</TreeView.ItemText> </TreeView.Item> )} </TreeView.NodeProvider> );}
export function Example() { return ( // variant/size resolve once on Root; expansion, selection, typeahead, // and depth-aware indentation come from the machine <TreeView.Root collection={collection} defaultExpandedValue={["src"]} size="md"> <TreeView.Tree> {collection.rootNode.children.map((node, index) => ( <TreeNode key={node.value} node={node} indexPath={[index]} /> ))} </TreeView.Tree> </TreeView.Root> );}Variants
src
components
card.tsx
package.json
README.md
src
components
card.tsx
package.json
README.md
Sizes
src
components
card.tsx
package.json
README.md
src
components
card.tsx
package.json
README.md
src
components
card.tsx
package.json
README.md
Install
1npx stylexblocks add tree-viewSource
components/ui/ark-ui/tree-view.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449import * as stylex from "@stylexjs/stylex";import { TreeView as ArkTreeView, useTreeViewNodeContext, type TreeNode, type TreeViewRootProps as ArkTreeViewRootProps,} from "@ark-ui/react/tree-view";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, fromMap, resolveSlotStyles, type SlotRecipeVariants, type WithStyleOverrides,} from "#lib/recipe-props.ts";import { treeViewTreeIconVars } from '@stylexblocks/ui/slot-recipes/tree-view.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const treeViewRoot = stylex.create({ base: { width: sizes.full, display: 'flex', flexDirection: 'column', gap: spacing._2, },})
const treeViewTree = stylex.create({ base: { display: 'flex', flexDirection: 'column', '--tree-item-gap': spacing._2, '--tree-indentation': 'calc(var(--tree-indent-size) + var(--tree-icon-size) * 0.5)', [treeViewTreeIconVars.width]: 'var(--tree-icon-size)', [treeViewTreeIconVars.height]: 'var(--tree-icon-size)', },})
const treeViewTreeSizes = stylex.create({ md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, '--tree-indent-size': spacing._4, '--tree-icon-size': spacing._4, '--tree-padding-inline': spacing._3, '--tree-padding-block': spacing._1_5, }, sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, '--tree-indent-size': spacing._4, '--tree-icon-size': spacing._3, '--tree-padding-inline': spacing._3, '--tree-padding-block': spacing._1, }, xs: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, '--tree-indent-size': spacing._4, '--tree-icon-size': spacing._3, '--tree-padding-inline': spacing._2, '--tree-padding-block': spacing._1, },})
const treeViewTreeIcon = stylex.create({ base: { width: treeViewTreeIconVars.width, height: treeViewTreeIconVars.height, },})
const treeViewLabel = stylex.create({ base: { fontWeight: fontWeights.medium, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, },})
const treeViewBranch = stylex.create({ base: { position: 'relative', },})
const treeViewBranchContent = stylex.create({ base: { position: 'relative', },})
const treeViewBranchContentAnimateContents = stylex.create({ true: { overflow: 'hidden', animationName: { default: null, ':is([data-open], [data-state="open"])': 'expand-height, fade-in', ':is([data-closed], [data-state="closed"])': 'collapse-height, fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.moderate, }, },})
const treeViewBranchIndentGuide = stylex.create({ base: { height: '100%', width: '1px', backgroundColor: semanticColors.border, position: 'absolute', '--tree-depth': 'calc(var(--depth) - 1)', '--tree-indentation-offset': 'calc(var(--tree-indentation) * var(--tree-depth))', '--tree-offset': 'calc(var(--tree-padding-inline) + var(--tree-indentation-offset))', insetInlineStart: 'calc(var(--tree-offset) + var(--tree-icon-size) * 0.5)', zIndex: 1, },})
const treeViewBranchIndicator = stylex.create({ base: { color: semanticColors.fgMuted, transformOrigin: 'center', transitionDuration: durations.moderate, transitionProperty: 'transform', transitionTimingFunction: 'ease-in-out', },})
const treeViewBranchTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', },})
const treeViewBranchControl = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: 'var(--tree-item-gap)', borderRadius: semanticRadii.controlMd, userSelect: 'none', position: 'relative', '--tree-depth': 'calc(var(--depth) - 1)', '--tree-indentation-offset': 'calc(var(--tree-indentation) * var(--tree-depth))', '--tree-offset': 'calc(var(--tree-padding-inline) + var(--tree-indentation-offset))', paddingInlineStart: 'var(--tree-offset)', paddingInlineEnd: 'var(--tree-padding-inline)', paddingBlock: 'var(--tree-padding-block)', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, '--focus-ring-color': semanticColors.borderEmphasized, '--focus-ring-width': '2px', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, },})
const treeViewBranchControlVariants = stylex.create({ subtle: {}, solid: {},})
const treeViewItem = stylex.create({ base: { display: 'flex', alignItems: 'center', gap: 'var(--tree-item-gap)', borderRadius: semanticRadii.controlMd, userSelect: 'none', position: 'relative', '--tree-depth': 'calc(var(--depth) - 1)', '--tree-indentation-offset': 'calc(var(--tree-indentation) * var(--tree-depth))', '--tree-offset': 'calc(var(--tree-padding-inline) + var(--tree-indentation-offset))', paddingInlineStart: 'var(--tree-offset)', paddingInlineEnd: 'var(--tree-padding-inline)', paddingBlock: 'var(--tree-padding-block)', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, '--focus-ring-color': semanticColors.borderEmphasized, '--focus-ring-width': '2px', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, },})
const treeViewItemVariants = stylex.create({ subtle: {}, solid: {},})
const treeViewItemText = stylex.create({ base: { flex: 1, },})
const treeViewBranchText = stylex.create({ base: { flex: 1, },})
const treeViewNodeCheckbox = stylex.create({ base: { display: 'inline-flex', },})
export type TreeViewSize = keyof typeof treeViewTreeSizes
export type TreeViewVariant = keyof typeof treeViewBranchControlVariants
export type TreeViewAnimateContent = keyof typeof treeViewBranchContentAnimateContents
// What the generated recipe doesn't carry: interactive row affordances and// the selected/expanded treatment (palette-aware). StyleX keys those off// `data-selected` / `data-state` that the node wrappers copy from the// machine's node context.const treeStatic = stylex.create({ row: { cursor: "pointer", color: semanticColors.fg, backgroundColor: { default: "transparent", ":hover": semanticColors.bgMuted }, }, indicatorExpanded: { transform: { default: null, ':is([data-state="open"])': "rotate(90deg)", }, },});
const treeRowSelected = stylex.create({ subtle: { backgroundColor: { default: null, ":is([data-selected])": colorPalette.muted, }, color: { default: null, ":is([data-selected])": colorPalette.fg, }, }, solid: { backgroundColor: { default: null, ":is([data-selected])": colorPalette.solid, }, color: { default: null, ":is([data-selected])": colorPalette.contrast, }, },});
const treeViewRecipe = defineSlotRecipe({ slots: { root: { styles: treeViewRoot }, tree: { styles: treeViewTree, sizes: treeViewTreeSizes }, label: { styles: treeViewLabel }, branch: { styles: treeViewBranch }, branchContent: { styles: treeViewBranchContent }, branchIndentGuide: { styles: treeViewBranchIndentGuide }, branchIndicator: { styles: treeViewBranchIndicator }, branchTrigger: { styles: treeViewBranchTrigger }, branchControl: { styles: treeViewBranchControl, variants: treeViewBranchControlVariants, }, branchText: { styles: treeViewBranchText }, item: { styles: treeViewItem, variants: treeViewItemVariants }, itemText: { styles: treeViewItemText }, nodeCheckbox: { styles: treeViewNodeCheckbox }, }, defaultVariants: { size: "md", variant: "subtle" },});
const { withProvider, withContext } = createSlotRecipeContext(treeViewRecipe);
const TreeViewRoot = withProvider(ArkTreeView.Root, "root", "TreeViewRoot");
type TreeViewVariants = SlotRecipeVariants<typeof treeViewRecipe>;
function Root<T extends TreeNode>( props: WithStyleOverrides<ArkTreeViewRootProps<T>> & Partial<TreeViewVariants>,) { return <TreeViewRoot {...(props as React.ComponentProps<typeof TreeViewRoot>)} />;}Root.displayName = "TreeViewRoot";
function BranchControlNode( props: React.ComponentProps<typeof ArkTreeView.BranchControl>,) { const node = useTreeViewNodeContext(); return ( <ArkTreeView.BranchControl {...props} data-selected={node.selected ? "" : undefined} /> );}
function BranchIndicatorNode( props: React.ComponentProps<typeof ArkTreeView.BranchIndicator>,) { const node = useTreeViewNodeContext(); return ( <ArkTreeView.BranchIndicator {...props} data-state={node.expanded ? "open" : "closed"} /> );}
function ItemNode(props: React.ComponentProps<typeof ArkTreeView.Item>) { const node = useTreeViewNodeContext(); return ( <ArkTreeView.Item {...props} data-selected={node.selected ? "" : undefined} /> );}
/** * TreeView — Ark UI's tree-view machine wearing the generated recipe. * `<TreeView.Root collection variant size>` resolves the recipe once and * every part inherits it through context; behavior (expand/collapse, * selection, typeahead, `--depth` indentation) is the machine's. Iterate * nodes with `TreeView.NodeProvider` and `createTreeCollection` from * `@ark-ui/react/tree-view`. Composition per part via `asChild`. */export const TreeView = { Root, Tree: withContext(ArkTreeView.Tree, "tree", "TreeViewTree"), Label: withContext(ArkTreeView.Label, "label", "TreeViewLabel"), NodeProvider: ArkTreeView.NodeProvider, Branch: withContext(ArkTreeView.Branch, "branch", "TreeViewBranch"), BranchControl: withContext( BranchControlNode, (v) => [ ...resolveSlotStyles(treeViewRecipe.slots.branchControl, v), treeStatic.row, fromMap(treeRowSelected, v.variant), ], "TreeViewBranchControl", ), // Ark's branch content is a collapsible under the hood: it sets `--height` // and `data-state`, so the recipe's animateContent styles apply cleanly. BranchContent: withContext( ArkTreeView.BranchContent, () => [treeViewBranchContent.base, treeViewBranchContentAnimateContents.true], "TreeViewBranchContent", ), BranchIndentGuide: withContext( ArkTreeView.BranchIndentGuide, "branchIndentGuide", "TreeViewBranchIndentGuide", ), BranchIndicator: withContext( BranchIndicatorNode, () => [treeViewBranchIndicator.base, treeStatic.indicatorExpanded], "TreeViewBranchIndicator", ), BranchTrigger: withContext( ArkTreeView.BranchTrigger, "branchTrigger", "TreeViewBranchTrigger", ), BranchText: withContext(ArkTreeView.BranchText, "branchText", "TreeViewBranchText"), Item: withContext( ItemNode, (v) => [ ...resolveSlotStyles(treeViewRecipe.slots.item, v), treeStatic.row, fromMap(treeRowSelected, v.variant), ], "TreeViewItem", ), ItemText: withContext(ArkTreeView.ItemText, "itemText", "TreeViewItemText"), NodeCheckbox: withContext( ArkTreeView.NodeCheckbox, "nodeCheckbox", "TreeViewNodeCheckbox", ), Icon: withContext("svg", () => [treeViewTreeIcon.base], "TreeViewIcon"),};