Components
Tree view

Tree View

A component that is used to show a tree hierarchy

Usage

  • Item 3
import { TreeView, type TreeViewData } from '~/components/ui'

export const Demo = () => {
  return <TreeView data={data} maxW="2xs" />
}

const data: TreeViewData = {
  label: 'Root',
  children: [
    {
      id: '1',
      name: 'Item 1',
      children: [
        {
          id: '1.1',
          name: 'Item 1.1',
        },
        {
          id: '1.2',
          name: 'Item 1.2',
          children: [
            {
              id: '1.2.1',
              name: 'Item 1.2.1',
            },
            {
              id: '1.2.2',
              name: 'Item 1.2.2',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Item 2',
      children: [
        {
          id: '2.1',
          name: 'Item 2.1',
        },
        {
          id: '2.2',
          name: 'Item 2.2',
        },
      ],
    },
    {
      id: '3',
      name: 'Item 3',
    },
  ],
}

Installation

1

Add Component

Insert code snippet into your project. Update import paths as needed.

import { TreeView as ArkTreeView, type TreeViewRootProps } from '@ark-ui/react/tree-view'
import { forwardRef } from 'react'
import { css, cx } from 'styled-system/css'
import { splitCssProps } from 'styled-system/jsx'
import { treeView } from 'styled-system/recipes'
import type { Assign, JsxStyleProps } from 'styled-system/types'

interface Child {
  id: string
  name: string
  children?: Child[]
}

export interface TreeViewData {
  label: string
  children: Child[]
}

export interface TreeViewProps extends Assign<JsxStyleProps, TreeViewRootProps> {
  data: TreeViewData
}

export const TreeView = forwardRef<HTMLDivElement, TreeViewProps>((props, ref) => {
  const [cssProps, localProps] = splitCssProps(props)
  const { data, className, ...rootProps } = localProps
  const styles = treeView()

  const renderChild = (child: Child) =>
    child.children ? (
      <ArkTreeView.Branch key={child.id} id={child.id} className={styles.branch}>
        <ArkTreeView.BranchControl className={styles.branchControl}>
          <ArkTreeView.BranchIndicator className={styles.branchIndicator}>
            <ChevronRightIcon />
          </ArkTreeView.BranchIndicator>
          <ArkTreeView.BranchText className={styles.branchText}>
            {child.name}
          </ArkTreeView.BranchText>
        </ArkTreeView.BranchControl>
        <ArkTreeView.BranchContent className={styles.branchContent}>
          {child.children.map(renderChild)}
        </ArkTreeView.BranchContent>
      </ArkTreeView.Branch>
    ) : (
      <ArkTreeView.Item key={child.id} id={child.id} className={styles.item}>
        <ArkTreeView.ItemText className={styles.itemText}>{child.name}</ArkTreeView.ItemText>
      </ArkTreeView.Item>
    )

  return (
    <ArkTreeView.Root
      ref={ref}
      aria-label={data.label}
      className={cx(styles.root, css(cssProps), className)}
      {...rootProps}
    >
      <ArkTreeView.Tree className={styles.tree}>{data.children.map(renderChild)}</ArkTreeView.Tree>
    </ArkTreeView.Root>
  )
})

TreeView.displayName = 'TreeView'

const ChevronRightIcon = () => (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
    <title>Chevron Right Icon</title>
    <path
      fill="none"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="2"
      d="m9 18l6-6l-6-6"
    />
  </svg>
)
2

Add Recipe

This step is necessary only if you do not use any of the Park UI plugins.

import { treeViewAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'

export const treeView = defineSlotRecipe({
  className: 'treeView',
  slots: treeViewAnatomy.keys(),
  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',
    },
    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',
    },
  },
})

Previous

Tooltip

On this page