Tree View
A component that is used to show a tree hierarchy
'use client'
import { createTreeCollection } from '@ark-ui/react/tree-view'
import { TreeView } from '~/components/ui/tree-view'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<Node>({
nodeToValue: (node) => node.id,
nodeToString: (node) => node.name,
rootNode: {
id: 'ROOT',
name: '',
children: [
{
id: 'node_modules',
name: 'node_modules',
children: [
{ id: 'node_modules/zag-js', name: 'zag-js' },
{ id: 'node_modules/pandacss', name: 'panda' },
{
id: 'node_modules/@types',
name: '@types',
children: [
{ id: 'node_modules/@types/react', name: 'react' },
{ id: 'node_modules/@types/react-dom', name: 'react-dom' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const Demo = () => {
return <TreeView collection={collection} maxW="2xs" />
}
import { createTreeCollection } from '@ark-ui/solid/tree-view'
import { TreeView } from '~/components/ui/tree-view'
interface Node {
id: string
name: string
children?: Node[]
}
const collection = createTreeCollection<Node>({
nodeToValue: (node) => node.id,
nodeToString: (node) => node.name,
rootNode: {
id: 'ROOT',
name: '',
children: [
{
id: 'node_modules',
name: 'node_modules',
children: [
{ id: 'node_modules/zag-js', name: 'zag-js' },
{ id: 'node_modules/pandacss', name: 'panda' },
{
id: 'node_modules/@types',
name: '@types',
children: [
{ id: 'node_modules/@types/react', name: 'react' },
{ id: 'node_modules/@types/react-dom', name: 'react-dom' },
],
},
],
},
{
id: 'src',
name: 'src',
children: [
{ id: 'src/app.tsx', name: 'app.tsx' },
{ id: 'src/index.ts', name: 'index.ts' },
],
},
{ id: 'panda.config', name: 'panda.config.ts' },
{ id: 'package.json', name: 'package.json' },
{ id: 'renovate.json', name: 'renovate.json' },
{ id: 'readme.md', name: 'README.md' },
],
},
})
export const Demo = () => {
return <TreeView collection={collection} maxW="2xs" />
}
Usage
import { TreeView } from '~/components/ui/tree-view'
Installation
npx @park-ui/cli components add tree-view
1
Add Styled Primitive
Copy the code snippet below into ~/components/ui/styled/tree-view.tsx
'use client'
import type { Assign } from '@ark-ui/react'
import { type TreeNode, TreeView } from '@ark-ui/react/tree-view'
import { type TreeViewVariantProps, treeView } from 'styled-system/recipes'
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withProvider, withContext } = createStyleContext(treeView)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
HTMLDivElement,
Assign<
Assign<HTMLStyledProps<'div'>, TreeView.RootProviderBaseProps<TreeNode>>,
TreeViewVariantProps
>
>(TreeView.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
HTMLDivElement,
Assign<Assign<HTMLStyledProps<'div'>, TreeView.RootBaseProps<TreeNode>>, TreeViewVariantProps>
>(TreeView.Root, 'root')
export const BranchContent = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchContentBaseProps>
>(TreeView.BranchContent, 'branchContent')
export const BranchControl = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchControlBaseProps>
>(TreeView.BranchControl, 'branchControl')
export const BranchIndicator = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchIndicatorBaseProps>
>(TreeView.BranchIndicator, 'branchIndicator')
export const Branch = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchBaseProps>
>(TreeView.Branch, 'branch')
export const BranchText = withContext<
HTMLSpanElement,
Assign<HTMLStyledProps<'span'>, TreeView.BranchTextBaseProps>
>(TreeView.BranchText, 'branchText')
export const BranchIndentGuide = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchIndentGuideBaseProps>
>(TreeView.BranchIndentGuide, 'branchIndentGuide')
export const BranchTrigger = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.BranchTriggerBaseProps>
>(TreeView.BranchTrigger, 'branchTrigger')
export const ItemIndicator = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.ItemIndicatorBaseProps>
>(TreeView.ItemIndicator, 'itemIndicator')
export const Item = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.ItemBaseProps>
>(TreeView.Item, 'item')
export const ItemText = withContext<
HTMLSpanElement,
Assign<HTMLStyledProps<'span'>, TreeView.ItemTextBaseProps>
>(TreeView.ItemText, 'itemText')
export const Label = withContext<
HTMLLabelElement,
Assign<HTMLStyledProps<'label'>, TreeView.LabelBaseProps>
>(TreeView.Label, 'label')
export const Tree = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, TreeView.TreeBaseProps>
>(TreeView.Tree, 'tree')
export type NodeProviderProps = TreeView.NodeProviderProps<TreeNode>
export const NodeProvider = TreeView.NodeProvider
export { TreeViewContext as Context } from '@ark-ui/react/tree-view'
import type { Assign } from '@ark-ui/solid'
import { type TreeNode, TreeView } from '@ark-ui/solid/tree-view'
import type { ComponentProps } from 'solid-js'
import { type TreeViewVariantProps, treeView } from 'styled-system/recipes'
import type { HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withProvider, withContext } = createStyleContext(treeView)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
Assign<
Assign<HTMLStyledProps<'div'>, TreeView.RootProviderBaseProps<TreeNode>>,
TreeViewVariantProps
>
>(TreeView.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
Assign<Assign<HTMLStyledProps<'div'>, TreeView.RootBaseProps<TreeNode>>, TreeViewVariantProps>
>(TreeView.Root, 'root')
export const BranchContent = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.BranchContentBaseProps>
>(TreeView.BranchContent, 'branchContent')
export const BranchControl = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.BranchControlBaseProps>
>(TreeView.BranchControl, 'branchControl')
export const BranchIndicator = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.BranchIndicatorBaseProps>
>(TreeView.BranchIndicator, 'branchIndicator')
export const Branch = withContext<Assign<HTMLStyledProps<'div'>, TreeView.BranchBaseProps>>(
TreeView.Branch,
'branch',
)
export const BranchIndentGuide = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.BranchIndentGuideBaseProps>
>(TreeView.BranchIndentGuide, 'branchIndentGuide')
export const BranchText = withContext<
Assign<HTMLStyledProps<'span'>, TreeView.BranchTextBaseProps>
>(TreeView.BranchText, 'branchText')
export const BranchTrigger = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.BranchTriggerBaseProps>
>(TreeView.BranchTrigger, 'branchTrigger')
export const ItemIndicator = withContext<
Assign<HTMLStyledProps<'div'>, TreeView.ItemIndicatorBaseProps>
>(TreeView.ItemIndicator, 'itemIndicator')
export const Item = withContext<Assign<HTMLStyledProps<'div'>, TreeView.ItemBaseProps>>(
TreeView.Item,
'item',
)
export const ItemText = withContext<Assign<HTMLStyledProps<'span'>, TreeView.ItemTextBaseProps>>(
TreeView.ItemText,
'itemText',
)
export const Label = withContext<Assign<HTMLStyledProps<'label'>, TreeView.LabelBaseProps>>(
TreeView.Label,
'label',
)
export const Tree = withContext<Assign<HTMLStyledProps<'div'>, TreeView.TreeBaseProps>>(
TreeView.Tree,
'tree',
)
export type NodeProviderProps = TreeView.NodeProviderProps<TreeNode>
export const NodeProvider = TreeView.NodeProvider
export { TreeViewContext as Context } from '@ark-ui/solid'
No snippet found
2
Add Composition
Copy the composition snippet below into ~/components/ui/tree-view.tsx
'use client'
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-react'
import { forwardRef } from 'react'
import * as StyledTreeView from './styled/tree-view'
export const TreeView = forwardRef<HTMLDivElement, StyledTreeView.RootProps>((props, ref) => {
return (
<StyledTreeView.Root ref={ref} {...props}>
<StyledTreeView.Tree>
{/* @ts-expect-error */}
{props.collection.rootNode.children.map((node, index) => (
<TreeNode key={node.id} node={node} indexPath={[index]} />
))}
</StyledTreeView.Tree>
</StyledTreeView.Root>
)
})
TreeView.displayName = 'TreeView'
const TreeNode = (props: StyledTreeView.NodeProviderProps) => {
const { node, indexPath } = props
return (
<StyledTreeView.NodeProvider key={node.id} node={node} indexPath={indexPath}>
{node.children ? (
<StyledTreeView.Branch>
<StyledTreeView.BranchControl>
<StyledTreeView.BranchText>
<FolderIcon /> {node.name}
</StyledTreeView.BranchText>
<StyledTreeView.BranchIndicator>
<ChevronRightIcon />
</StyledTreeView.BranchIndicator>
</StyledTreeView.BranchControl>
<StyledTreeView.BranchContent>
<StyledTreeView.BranchIndentGuide />
{/* @ts-expect-error */}
{node.children.map((child, index) => (
<TreeNode key={child.id} node={child} indexPath={[...indexPath, index]} />
))}
</StyledTreeView.BranchContent>
</StyledTreeView.Branch>
) : (
<StyledTreeView.Item>
<StyledTreeView.ItemIndicator>
<CheckSquareIcon />
</StyledTreeView.ItemIndicator>
<StyledTreeView.ItemText>
<FileIcon />
{node.name}
</StyledTreeView.ItemText>
</StyledTreeView.Item>
)}
</StyledTreeView.NodeProvider>
)
}
import { CheckSquareIcon, ChevronRightIcon, FileIcon, FolderIcon } from 'lucide-solid'
import { For, Show } from 'solid-js'
import * as StyledTreeView from './styled/tree-view'
export const TreeView = (props: StyledTreeView.RootProps) => {
return (
<StyledTreeView.Root {...props}>
<StyledTreeView.Tree>
<For each={props.collection.rootNode.children}>
{(node, index) => <TreeNode node={node} indexPath={[index()]} />}
</For>
</StyledTreeView.Tree>
</StyledTreeView.Root>
)
}
const TreeNode = (props: StyledTreeView.NodeProviderProps) => {
const { node, indexPath } = props
return (
<StyledTreeView.NodeProvider node={node} indexPath={indexPath}>
<Show
when={node.children}
fallback={
<StyledTreeView.Item>
<StyledTreeView.ItemIndicator>
<CheckSquareIcon />
</StyledTreeView.ItemIndicator>
<StyledTreeView.ItemText>
<FileIcon />
{node.name}
</StyledTreeView.ItemText>
</StyledTreeView.Item>
}
>
<StyledTreeView.Branch>
<StyledTreeView.BranchControl>
<StyledTreeView.BranchText>
<FolderIcon /> {node.name}
</StyledTreeView.BranchText>
<StyledTreeView.BranchIndicator>
<ChevronRightIcon />
</StyledTreeView.BranchIndicator>
</StyledTreeView.BranchControl>
<StyledTreeView.BranchContent>
<StyledTreeView.BranchIndentGuide />
<For each={node.children}>
{(child, index) => <TreeNode node={child} indexPath={[...indexPath, index()]} />}
</For>
</StyledTreeView.BranchContent>
</StyledTreeView.Branch>
</Show>
</StyledTreeView.NodeProvider>
)
}
3
Integrate Recipe
If you're not using @park-ui/preset
, add the following recipe to yourpanda.config.ts
:
import { treeViewAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'
export const treeView = defineSlotRecipe({
className: 'treeView',
slots: [...treeViewAnatomy.keys(), 'branchIndentGuide'],
base: {
root: {
width: 'full',
},
branch: {
"&[data-depth='1'] > [data-part='branch-content']": {
_before: {
bg: 'border.default',
content: '""',
height: 'full',
left: '3',
position: 'absolute',
width: '1px',
zIndex: '1',
},
},
},
branchContent: {
position: 'relative',
},
branchIndentGuide: {},
branchControl: {
alignItems: 'center',
borderRadius: 'l2',
color: 'fg.muted',
display: 'flex',
fontWeight: 'medium',
gap: '1.5',
ps: 'calc((var(--depth) - 1) * 22px)',
py: '1.5',
textStyle: 'sm',
transitionDuration: 'normal',
transitionProperty: 'background, color',
transitionTimingFunction: 'default',
"&[data-depth='1']": {
ps: '1',
},
"&[data-depth='1'] > [data-part='branch-text'] ": {
fontWeight: 'semibold',
color: 'fg.default',
},
_hover: {
background: 'gray.a2',
color: 'fg.default',
},
},
branchIndicator: {
color: 'accent.default',
transformOrigin: 'center',
transitionDuration: 'normal',
transitionProperty: 'transform',
transitionTimingFunction: 'default',
'& svg': {
fontSize: 'md',
width: '4',
height: '4',
},
_open: {
transform: 'rotate(90deg)',
},
},
item: {
borderRadius: 'l2',
color: 'fg.muted',
cursor: 'pointer',
fontWeight: 'medium',
position: 'relative',
ps: 'calc(((var(--depth) - 1) * 22px) + 22px)',
py: '1.5',
textStyle: 'sm',
transitionDuration: 'normal',
transitionProperty: 'background, color',
transitionTimingFunction: 'default',
"&[data-depth='1']": {
ps: '6',
fontWeight: 'semibold',
color: 'fg.default',
_selected: {
_before: {
bg: 'transparent',
},
},
},
_hover: {
background: 'gray.a2',
color: 'fg.default',
},
_selected: {
background: 'accent.a2',
color: 'accent.text',
_hover: {
background: 'accent.a2',
color: 'accent.text',
},
_before: {
content: '""',
position: 'absolute',
left: '3',
top: '0',
width: '2px',
height: 'full',
bg: 'accent.default',
zIndex: '1',
},
},
},
tree: {
display: 'flex',
flexDirection: 'column',
gap: '3',
},
},
})