beta

Tree

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

installyarn add react-aria-components
version1.4.0
usageimport {UNSTABLE_Tree} from 'react-aria-components'

Example#


import {
  UNSTABLE_Tree as Tree,
  UNSTABLE_TreeItem as TreeItem,
  UNSTABLE_TreeItemContent as TreeItemContent,
  Button,
  Collection
} from 'react-aria-components';
import {MyCheckbox} from './Checkbox';

let items = [
  {id: 1, title: 'Documents', children: [
    {id: 2, title: 'Project', children: [
      {id: 3, title: 'Weekly Report', children: []}
    ]}
  ]},
  {id: 4, title: 'Photos', children: [
    {id: 5, title: 'Image 1', children: []},
    {id: 6, title: 'Image 2', children: []}
  ]}
];

<Tree aria-label="Files" selectionMode="multiple" items={items}>
  {function renderItem(item) {
    return (
      <TreeItem textValue={item.title}>
        <TreeItemContent>
          {item.children.length ? <Button slot="chevron">
            <svg viewBox="0 0 24 24">
              <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
            </svg>
          </Button> : null}
          <MyCheckbox slot="selection" />
          {item.title}
          <Button aria-label="Info"></Button>
        </TreeItemContent>
        <Collection items={item.children}>
          {renderItem}
        </Collection>
      </TreeItem>
    );
  }}
</Tree>
import {
  Button,
  Collection,
  UNSTABLE_Tree as Tree,
  UNSTABLE_TreeItem as TreeItem,
  UNSTABLE_TreeItemContent as TreeItemContent
} from 'react-aria-components';
import {MyCheckbox} from './Checkbox';

let items = [
  {
    id: 1,
    title: 'Documents',
    children: [
      {
        id: 2,
        title: 'Project',
        children: [
          { id: 3, title: 'Weekly Report', children: [] }
        ]
      }
    ]
  },
  {
    id: 4,
    title: 'Photos',
    children: [
      { id: 5, title: 'Image 1', children: [] },
      { id: 6, title: 'Image 2', children: [] }
    ]
  }
];

<Tree
  aria-label="Files"
  selectionMode="multiple"
  items={items}
>
  {function renderItem(item) {
    return (
      <TreeItem textValue={item.title}>
        <TreeItemContent>
          {item.children.length
            ? (
              <Button slot="chevron">
                <svg viewBox="0 0 24 24">
                  <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
                </svg>
              </Button>
            )
            : null}
          <MyCheckbox slot="selection" />
          {item.title}
          <Button aria-label="Info"></Button>
        </TreeItemContent>
        <Collection items={item.children}>
          {renderItem}
        </Collection>
      </TreeItem>
    );
  }}
</Tree>
import {
  Button,
  Collection,
  UNSTABLE_Tree as Tree,
  UNSTABLE_TreeItem
    as TreeItem,
  UNSTABLE_TreeItemContent
    as TreeItemContent
} from 'react-aria-components';
import {MyCheckbox} from './Checkbox';

let items = [
  {
    id: 1,
    title: 'Documents',
    children: [
      {
        id: 2,
        title: 'Project',
        children: [
          {
            id: 3,
            title:
              'Weekly Report',
            children: []
          }
        ]
      }
    ]
  },
  {
    id: 4,
    title: 'Photos',
    children: [
      {
        id: 5,
        title: 'Image 1',
        children: []
      },
      {
        id: 6,
        title: 'Image 2',
        children: []
      }
    ]
  }
];

<Tree
  aria-label="Files"
  selectionMode="multiple"
  items={items}
>
  {function renderItem(
    item
  ) {
    return (
      <TreeItem
        textValue={item
          .title}
      >
        <TreeItemContent>
          {item
              .children
              .length
            ? (
              <Button slot="chevron">
                <svg viewBox="0 0 24 24">
                  <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
                </svg>
              </Button>
            )
            : null}
          <MyCheckbox slot="selection" />
          {item.title}
          <Button aria-label="Info"></Button>
        </TreeItemContent>
        <Collection
          items={item
            .children}
        >
          {renderItem}
        </Collection>
      </TreeItem>
    );
  }}
</Tree>
Show CSS
.react-aria-Tree {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: auto;
  padding: 4px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--overlay-background);
  forced-color-adjust: none;
  outline: none;
  width: 250px;
  max-height: 300px;
  box-sizing: border-box;

  &[data-focus-visible] {
    outline: 2px solid var(--focus-ring-color);
    outline-offset: -1px;
  }

  .react-aria-TreeItem {
    display: flex;
    align-items: center;
    gap: 0.571rem;
    min-height: 28px;
    padding: 0.286rem 0.286rem 0.286rem 0.571rem;
    --padding: 20px;
    padding-left: calc((var(--tree-item-level) - 1) * 20px + 0.571rem + var(--padding));
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    transform: translateZ(0);

    &[data-has-child-rows] {
      --padding: 0px;
    }

    .react-aria-Button[slot=chevron] {
      all: unset;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 1.143rem;
      height: 1.143rem;

      svg {
        rotate: 0deg;
        transition: rotate 200ms;
        width: 12px;
        height: 12px;
        fill: none;
        stroke: currentColor;
        stroke-width: 3px;
      }
    }

    &[data-expanded] .react-aria-Button[slot=chevron] svg {
      rotate: 90deg;
    }

    &[data-focus-visible] {
      outline: 2px solid var(--focus-ring-color);
      outline-offset: -2px;
    }

    &[data-pressed] {
      background: var(--gray-100);
    }

    &[data-selected] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
      --focus-ring-color: var(--highlight-foreground);

      &[data-focus-visible] {
        outline-color: var(--highlight-foreground);
        outline-offset: -4px;
      }

      .react-aria-Button {
        color: var(--highlight-foreground);
        --highlight-hover: rgb(255 255 255 / 0.1);
        --highlight-pressed: rgb(255 255 255 / 0.2);
      }
    }

    &[data-disabled] {
      color: var(--text-color-disabled);
    }

    .react-aria-Button:not([slot]) {
      margin-left: auto;
      background: transparent;
      border: none;
      font-size: 1.2rem;
      line-height: 1.2em;
      padding: 0.286rem 0.429rem;
      transition: background 200ms;

      &[data-hovered] {
        background: var(--highlight-hover);
      }

      &[data-pressed] {
        background: var(--highlight-pressed);
        box-shadow: none;
      }
    }
  }

  /* join selected items if :has selector is supported */
  @supports selector(:has(.foo)) {
    gap: 0;

    .react-aria-TreeItem[data-selected]:has(+ [data-selected]) {
      border-end-start-radius: 0;
      border-end-end-radius: 0;
    }

    .react-aria-TreeItem[data-selected] + [data-selected] {
      border-start-start-radius: 0;
      border-start-end-radius: 0;
    }
  }

  :where(.react-aria-TreeItem) .react-aria-Checkbox {
    --selected-color: var(--highlight-foreground);
    --selected-color-pressed: var(--highlight-foreground-pressed);
    --checkmark-color: var(--highlight-background);
    --background-color: var(--highlight-background);
  }
}
.react-aria-Tree {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: auto;
  padding: 4px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--overlay-background);
  forced-color-adjust: none;
  outline: none;
  width: 250px;
  max-height: 300px;
  box-sizing: border-box;

  &[data-focus-visible] {
    outline: 2px solid var(--focus-ring-color);
    outline-offset: -1px;
  }

  .react-aria-TreeItem {
    display: flex;
    align-items: center;
    gap: 0.571rem;
    min-height: 28px;
    padding: 0.286rem 0.286rem 0.286rem 0.571rem;
    --padding: 20px;
    padding-left: calc((var(--tree-item-level) - 1) * 20px + 0.571rem + var(--padding));
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    transform: translateZ(0);

    &[data-has-child-rows] {
      --padding: 0px;
    }

    .react-aria-Button[slot=chevron] {
      all: unset;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 1.143rem;
      height: 1.143rem;

      svg {
        rotate: 0deg;
        transition: rotate 200ms;
        width: 12px;
        height: 12px;
        fill: none;
        stroke: currentColor;
        stroke-width: 3px;
      }
    }

    &[data-expanded] .react-aria-Button[slot=chevron] svg {
      rotate: 90deg;
    }

    &[data-focus-visible] {
      outline: 2px solid var(--focus-ring-color);
      outline-offset: -2px;
    }

    &[data-pressed] {
      background: var(--gray-100);
    }

    &[data-selected] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
      --focus-ring-color: var(--highlight-foreground);

      &[data-focus-visible] {
        outline-color: var(--highlight-foreground);
        outline-offset: -4px;
      }

      .react-aria-Button {
        color: var(--highlight-foreground);
        --highlight-hover: rgb(255 255 255 / 0.1);
        --highlight-pressed: rgb(255 255 255 / 0.2);
      }
    }

    &[data-disabled] {
      color: var(--text-color-disabled);
    }

    .react-aria-Button:not([slot]) {
      margin-left: auto;
      background: transparent;
      border: none;
      font-size: 1.2rem;
      line-height: 1.2em;
      padding: 0.286rem 0.429rem;
      transition: background 200ms;

      &[data-hovered] {
        background: var(--highlight-hover);
      }

      &[data-pressed] {
        background: var(--highlight-pressed);
        box-shadow: none;
      }
    }
  }

  /* join selected items if :has selector is supported */
  @supports selector(:has(.foo)) {
    gap: 0;

    .react-aria-TreeItem[data-selected]:has(+ [data-selected]) {
      border-end-start-radius: 0;
      border-end-end-radius: 0;
    }

    .react-aria-TreeItem[data-selected] + [data-selected] {
      border-start-start-radius: 0;
      border-start-end-radius: 0;
    }
  }

  :where(.react-aria-TreeItem) .react-aria-Checkbox {
    --selected-color: var(--highlight-foreground);
    --selected-color-pressed: var(--highlight-foreground-pressed);
    --checkmark-color: var(--highlight-background);
    --background-color: var(--highlight-background);
  }
}
.react-aria-Tree {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow: auto;
  padding: 4px;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--overlay-background);
  forced-color-adjust: none;
  outline: none;
  width: 250px;
  max-height: 300px;
  box-sizing: border-box;

  &[data-focus-visible] {
    outline: 2px solid var(--focus-ring-color);
    outline-offset: -1px;
  }

  .react-aria-TreeItem {
    display: flex;
    align-items: center;
    gap: 0.571rem;
    min-height: 28px;
    padding: 0.286rem 0.286rem 0.286rem 0.571rem;
    --padding: 20px;
    padding-left: calc((var(--tree-item-level) - 1) * 20px + 0.571rem + var(--padding));
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    transform: translateZ(0);

    &[data-has-child-rows] {
      --padding: 0px;
    }

    .react-aria-Button[slot=chevron] {
      all: unset;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 1.143rem;
      height: 1.143rem;

      svg {
        rotate: 0deg;
        transition: rotate 200ms;
        width: 12px;
        height: 12px;
        fill: none;
        stroke: currentColor;
        stroke-width: 3px;
      }
    }

    &[data-expanded] .react-aria-Button[slot=chevron] svg {
      rotate: 90deg;
    }

    &[data-focus-visible] {
      outline: 2px solid var(--focus-ring-color);
      outline-offset: -2px;
    }

    &[data-pressed] {
      background: var(--gray-100);
    }

    &[data-selected] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
      --focus-ring-color: var(--highlight-foreground);

      &[data-focus-visible] {
        outline-color: var(--highlight-foreground);
        outline-offset: -4px;
      }

      .react-aria-Button {
        color: var(--highlight-foreground);
        --highlight-hover: rgb(255 255 255 / 0.1);
        --highlight-pressed: rgb(255 255 255 / 0.2);
      }
    }

    &[data-disabled] {
      color: var(--text-color-disabled);
    }

    .react-aria-Button:not([slot]) {
      margin-left: auto;
      background: transparent;
      border: none;
      font-size: 1.2rem;
      line-height: 1.2em;
      padding: 0.286rem 0.429rem;
      transition: background 200ms;

      &[data-hovered] {
        background: var(--highlight-hover);
      }

      &[data-pressed] {
        background: var(--highlight-pressed);
        box-shadow: none;
      }
    }
  }

  /* join selected items if :has selector is supported */
  @supports selector(:has(.foo)) {
    gap: 0;

    .react-aria-TreeItem[data-selected]:has(+ [data-selected]) {
      border-end-start-radius: 0;
      border-end-end-radius: 0;
    }

    .react-aria-TreeItem[data-selected] + [data-selected] {
      border-start-start-radius: 0;
      border-start-end-radius: 0;
    }
  }

  :where(.react-aria-TreeItem) .react-aria-Checkbox {
    --selected-color: var(--highlight-foreground);
    --selected-color-pressed: var(--highlight-foreground-pressed);
    --checkmark-color: var(--highlight-background);
    --background-color: var(--highlight-background);
  }
}

Props#


Tree#

NameTypeDefaultDescription
selectionBehaviorSelectionBehaviorHow multiple selection should behave in the tree.
renderEmptyState( (props: TreeEmptyStateRenderProps )) => ReactNodeProvides content to display when there are no items in the list.
disabledBehaviorDisabledBehavior'selection'Whether disabledKeys applies to all interactions, or only selection.
itemsIterable<T>Item objects in the collection.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether the collection allows empty selection.
selectedKeys'all'Iterable<Key>The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>The initial selected keys in the collection (uncontrolled).
childrenReactNode( (item: object )) => ReactNodeThe contents of the collection.
dependenciesany[]Values that should invalidate the item cache when using dynamic collections.
classNamestring( (values: TreeRenderProps{
defaultClassName: stringundefined
} )) => string
The CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TreeRenderProps{
defaultStyle: CSSProperties
} )) => CSSPropertiesundefined
The inline style for the element. A function may be provided to compute the style based on component state.
expandedKeysIterable<Key>The currently expanded keys in the collection (controlled).
defaultExpandedKeysIterable<Key>The initial expanded keys in the collection (uncontrolled).
Events
NameTypeDescription
onAction( (key: Key )) => void

Handler that is called when a user performs an action on an item. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.

onSelectionChange( (keys: Selection )) => voidHandler that is called when the selection changes.
onScroll( (e: UIEvent<Element> )) => voidHandler that is called when a user scrolls. See MDN.
onExpandedChange( (keys: Set<Key> )) => anyHandler that is called when items are expanded or collapsed.
Layout
NameTypeDescription
slotstringnull

A slot name for the component. Slots allow the component to receive props from a parent component. An explicit null value indicates that the local props completely override all props received from a parent.

Accessibility
NameTypeDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

TreeItem#

NameTypeDescription
textValuestringA string representation of the tree item's contents, used for features like typeahead.
childrenReactNodeThe content of the tree item along with any nested children. Supports static nested tree items or use of a Collection to dynamically render nested tree items.
idKeyThe unique id of the tree row.
valueobjectThe object value that this tree item represents. When using dynamic collections, this is set automatically.
classNamestring( (values: TreeItemRenderProps{
defaultClassName: stringundefined
} )) => string
The CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: TreeItemRenderProps{
defaultStyle: CSSProperties
} )) => CSSPropertiesundefined
The inline style for the element. A function may be provided to compute the style based on component state.
hrefHrefA URL to link to. See MDN.
hrefLangstringHints at the human language of the linked URL. SeeMDN.
targetHTMLAttributeAnchorTargetThe target window for the link. See MDN.
relstringThe relationship between the linked resource and the current page. See MDN.
downloadbooleanstringCauses the browser to download the linked URL. A string may be provided to suggest a file name. See MDN.
pingstringA space-separated list of URLs to ping when the link is followed. See MDN.
referrerPolicyHTMLAttributeReferrerPolicyHow much of the referrer to send when following the link. See MDN.
routerOptionsRouterOptionsOptions for the configured client side router.
Events
NameTypeDescription
onHoverStart( (e: HoverEvent )) => voidHandler that is called when a hover interaction starts.
onHoverEnd( (e: HoverEvent )) => voidHandler that is called when a hover interaction ends.
onHoverChange( (isHovering: boolean )) => voidHandler that is called when the hover state changes.
Accessibility
NameTypeDescription
aria-labelstringAn accessibility label for this tree item.

TreeItemContent#

NameTypeDescription
childrenReactNode( (values: T{
defaultChildren: ReactNodeundefined
} )) => ReactNode
The children of the component. A function may be provided to alter the children based on component state.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-Tree {
  /* ... */
}
.react-aria-Tree {
  /* ... */
}
.react-aria-Tree {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<TreeItem className="my-tree-item">
  {/* ... */}
</TreeItem>
<TreeItem className="my-tree-item">
  {/* ... */}
</TreeItem>
<TreeItem className="my-tree-item">
  {/* ... */}
</TreeItem>

In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example:

.react-aria-TreeItem[data-expanded] {
  /* ... */
}

.react-aria-TreeItem[data-selected] {
  /* ... */
}
.react-aria-TreeItem[data-expanded] {
  /* ... */
}

.react-aria-TreeItem[data-selected] {
  /* ... */
}
.react-aria-TreeItem[data-expanded] {
  /* ... */
}

.react-aria-TreeItem[data-selected] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<TreeItem
  className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
/>
<TreeItem
  className={({ isSelected }) =>
    isSelected ? 'bg-blue-400' : 'bg-gray-100'}
/>
<TreeItem
  className={(
    { isSelected }
  ) =>
    isSelected
      ? 'bg-blue-400'
      : 'bg-gray-100'}
/>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a checkbox only when selection is enabled.

<TreeItem>
  {({selectionMode}) => (
    <>
      {selectionMode !== 'none' && <Checkbox />}
      Item
    </>
  )}
</TreeItem>
<TreeItem>
  {({selectionMode}) => (
    <>
      {selectionMode !== 'none' && <Checkbox />}
      Item
    </>
  )}
</TreeItem>
<TreeItem>
  {(
    { selectionMode }
  ) => (
    <>
      {selectionMode !==
          'none' && (
        <Checkbox />
      )}
      Item
    </>
  )}
</TreeItem>

The states, selectors, and render props for each component used in a Tree are documented below.

Tree#

A Tree can be targeted with the .react-aria-Tree CSS selector, or by overriding with a custom className. It supports the following states:

NameCSS SelectorDescription
isEmpty[data-empty]Whether the tree has no items and should display its empty state.
isFocused[data-focused]Whether the tree is currently focused.
isFocusVisible[data-focus-visible]Whether the tree is currently keyboard focused.
stateState of the tree.

TreeItem#

A TreeItem can be targeted with the .react-aria-TreeItem CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
isExpandedWhether the tree item is expanded.
isFocusVisibleWithinWhether the tree item's children have keyboard focus.
isHovered[data-hovered]Whether the item is currently hovered with a mouse.
isPressed[data-pressed]Whether the item is currently in a pressed state.
isSelected[data-selected]Whether the item is currently selected.
isFocused[data-focused]Whether the item is currently focused.
isFocusVisible[data-focus-visible]Whether the item is currently keyboard focused.
isDisabled[data-disabled]

Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on disabledKeys and disabledBehavior.

selectionMode[data-selection-mode="single | multiple"]The type of selection that is allowed in the collection.
selectionBehaviorThe selection behavior for the collection.

TreeItem also exposes a --tree-item-level CSS custom property, which you can use to adjust the indentation.

.react-aria-TreeItem {
  padding-left: calc((var(--tree-item-level) - 1) * 20px);
}
.react-aria-TreeItem {
  padding-left: calc((var(--tree-item-level) - 1) * 20px);
}
.react-aria-TreeItem {
  padding-left: calc((var(--tree-item-level) - 1) * 20px);
}