Beta Preview

Tree

A tree provides users with a way to navigate nested hierarchical information, with support for keyboard navigation and selection.

selectionMode 
Example
Tree.tsx
Tree.css
import {Tree, TreeItem} from './Tree';
import {Button} from './Button';

<Tree
  selectionMode="multiple"
  aria-label="Files">
  <TreeItem title="Documents">
    <TreeItem title="Project">
      <TreeItem title="Weekly Report" />
    </TreeItem>
  </TreeItem>
  <TreeItem title="Photos">
    <TreeItem title="Image 1" />
    <TreeItem title="Image 2" />
  </TreeItem>
</Tree>

Content

Tree follows the Collection Components API, accepting both static and dynamic collections. This example shows a dynamic collection, passing a list of objects to the items prop, and a recursive function to render the children.

Documents
import {Tree, TreeItem} from './Tree';
import {Collection} from 'react-aria-components';

<Tree aria-label="Files" defaultExpandedKeys={[1, 4]} items={items} selectionMode="multiple"> {function renderItem(item) {
return ( <TreeItem title={item.title}> {/* recursively render children */} <Collection items={item.children}> {renderItem} </Collection> </TreeItem> ); }} </Tree>

Asynchronous loading

Use renderEmptyState to display a spinner during initial load. To enable infinite scrolling, render a <TreeLoadMoreItem> at the end of each <TreeItem>. Use whatever data fetching library you prefer – this example uses useAsyncList from react-stately.

import {Collection, TreeLoadMoreItem} from 'react-aria-components';
import {Tree, TreeItem} from './Tree';
import {ProgressCircle} from './ProgressCircle';
import {useAsyncList} from 'react-stately';

interface Character {
  name: string
}

function AsyncLoadingExample() {
return ( <Tree aria-label="Async loading tree" style={{height: 300}}> <TreeItem title="Pokemon"> <Collection items={pokemonList.items}> {(item) => <TreeItem id={item.name} title={item.name} />}
</Collection> <TreeLoadMoreItem onLoadMore={pokemonList.loadMore} isLoading={pokemonList.loadingState === 'loadingMore'}> <ProgressCircle isIndeterminate aria-label="Loading more..." /> </TreeLoadMoreItem> </TreeItem> <TreeItem title="Star Wars"> <Collection items={starWarsList.items}> {(item) => <TreeItem id={item.name} title={item.name} />} </Collection> <TreeLoadMoreItem onLoadMore={starWarsList.loadMore} isLoading={starWarsList.loadingState === 'loadingMore'}> <ProgressCircle isIndeterminate aria-label="Loading more..." /> </TreeLoadMoreItem> </TreeItem> </Tree> ); }

Use the href prop on a <TreeItem> to create a link. See the client side routing guide to learn how to integrate with your framework. Link interactions vary depending on the selection behavior. See the selection guide for more details.

Bulbasaur
Ivysaur
Venusaur
selectionBehavior 
import {Tree, TreeItem} from './Tree';

<Tree
  selectionMode="multiple"
  aria-label="Tree with links"
  defaultExpandedKeys={['bulbasaur', 'ivysaur']}>
  <TreeItem
    href="https://pokemondb.net/pokedex/bulbasaur"
    target="_blank"
    id="bulbasaur"
    title="Bulbasaur">
    <TreeItem
      id="ivysaur"
      title="Ivysaur"
      href="https://pokemondb.net/pokedex/ivysaur"
      target="_blank">
      <TreeItem
        id="venusaur"
        title="Venusaur"
        href="https://pokemondb.net/pokedex/venusaur"
        target="_blank" />
    </TreeItem>
  </TreeItem>
</Tree>

Empty state

No results found.
import {Tree} from './Tree';

<Tree
  aria-label="Search results"
  renderEmptyState={() => 'No results found.'}>
  {[]}
</Tree>

Selection and actions

Use the selectionMode prop to enable single or multiple selection. The selected items can be controlled via the selectedKeys prop, matching the id prop of the items. The onAction event handles item actions. Items can be disabled with the isDisabled prop. See the selection guide for more details.

Bulbasaur
Ivysaur
Venusaur

Current selection:

selectionMode 
selectionBehavior 
disabledBehavior 
disallowEmptySelection 
import type {Selection} from 'react-aria-components';
import {Tree, TreeItem} from './Tree';
import {useState} from 'react';

function Example(props) {
  let [selected, setSelected] = useState<Selection>(new Set());

  return (
    <div>
      <Tree
        {...props}
        aria-label="Pokemon evolution"
        style={{height: 250}}
        defaultExpandedKeys={['bulbasaur', 'ivysaur']}
        selectionMode="multiple"
        selectedKeys={selected}
        onSelectionChange={setSelected}
        onAction={key => alert(`Clicked ${key}`)}
      >
        <TreeItem id="bulbasaur" title="Bulbasaur">
          <TreeItem id="ivysaur" title="Ivysaur">
            <TreeItem id="venusaur" title="Venusaur" isDisabled />
          </TreeItem>
        </TreeItem>
        <TreeItem id="charmander" title="Charmander">
          <TreeItem id="charmeleon" title="Charmeleon">
            <TreeItem id="charizard" title="Charizard" />
          </TreeItem>
        </TreeItem>
        <TreeItem id="squirtle" title="Squirtle">
          <TreeItem id="wartortle" title="Wartortle">
            <TreeItem id="blastoise" title="Blastoise" />
          </TreeItem>
        </TreeItem>
      </Tree>
      <p>Current selection: {selected === 'all' ? 'all' : [...selected].join(', ')}</p>
    </div>
  );
}

Drag and drop

Tree supports drag and drop interactions when the dragAndDropHooks prop is provided using the hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the drag and drop guide to learn more.

import {useTreeData} from 'react-stately';
import {Tree, TreeItem} from './Tree';
import {useDragAndDrop, Collection} from 'react-aria-components';

function Example() {
let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => ({'text/plain': tree.getItem(key).value.title})), onMove(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } else if (e.target.dropPosition === 'on') { // Move items to become children of the target let targetNode = tree.getItem(e.target.key); if (targetNode) { let targetIndex = targetNode.children ? targetNode.children.length : 0; let keyArray = Array.from(e.keys); for (let i = 0; i < keyArray.length; i++) { tree.move(keyArray[i], e.target.key, targetIndex + i); } } } } }); return ( <Tree aria-label="Tree with hierarchical drag and drop" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <TreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </TreeItem> ) }} </Tree> ); }

API

DocumentsTreeItemCheckbox (optional)12 itemsOnboardingPDFBudgetXLSSales PitchPPTCollapse and expand buttonTree