Tree
A tree provides users with a way to navigate nested hierarchical information, with support for keyboard navigation and selection.
install | yarn add react-aria-components |
---|---|
version | 1.4.0 |
usage | import {UNSTABLE_Tree} from 'react-aria-components' |
Under construction
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#
Name | Type | Default | Description |
selectionBehavior | SelectionBehavior | — | How multiple selection should behave in the tree. |
renderEmptyState | (
(props: TreeEmptyStateRenderProps
)) => ReactNode | — | Provides content to display when there are no items in the list. |
disabledBehavior | DisabledBehavior | 'selection' | Whether disabledKeys applies to all interactions, or only selection. |
items | Iterable<T> | — | Item objects in the collection. |
disabledKeys | Iterable<Key> | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
selectionMode | SelectionMode | — | The type of selection that is allowed in the collection. |
disallowEmptySelection | boolean | — | Whether 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). |
children | ReactNode | (
(item: object
)) => ReactNode | — | The contents of the collection. |
dependencies | any[] | — | Values that should invalidate the item cache when using dynamic collections. |
className | string | (
(values: TreeRenderProps
& & {}
)) => string | — | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: TreeRenderProps
& & {}
)) => CSSProperties | undefined | — | The inline style for the element. A function may be provided to compute the style based on component state. |
expandedKeys | Iterable<Key> | — | The currently expanded keys in the collection (controlled). |
defaultExpandedKeys | Iterable<Key> | — | The initial expanded keys in the collection (uncontrolled). |
Events
Name | Type | Description |
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 |
onSelectionChange | (
(keys: Selection
)) => void | Handler that is called when the selection changes. |
onScroll | (
(e: UIEvent<Element>
)) => void | Handler that is called when a user scrolls. See MDN. |
onExpandedChange | (
(keys: Set<Key>
)) => any | Handler that is called when items are expanded or collapsed. |
Layout
Name | Type | Description |
slot | string | null | A slot name for the component. Slots allow the component to receive props from a parent component.
An explicit |
Accessibility
Name | Type | Description |
id | string | The element's unique identifier. See MDN. |
aria-label | string | Defines a string value that labels the current element. |
aria-labelledby | string | Identifies the element (or elements) that labels the current element. |
aria-describedby | string | Identifies the element (or elements) that describes the object. |
aria-details | string | Identifies the element (or elements) that provide a detailed, extended description for the object. |
TreeItem#
Name | Type | Description |
textValue | string | A string representation of the tree item's contents, used for features like typeahead. |
children | ReactNode | The 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. |
id | Key | The unique id of the tree row. |
value | object | The object value that this tree item represents. When using dynamic collections, this is set automatically. |
className | string | (
(values: TreeItemRenderProps
& & {}
)) => string | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: TreeItemRenderProps
& & {}
)) => CSSProperties | undefined | The inline style for the element. A function may be provided to compute the style based on component state. |
href | Href | A URL to link to. See MDN. |
hrefLang | string | Hints at the human language of the linked URL. SeeMDN. |
target | HTMLAttributeAnchorTarget | The target window for the link. See MDN. |
rel | string | The relationship between the linked resource and the current page. See MDN. |
download | boolean | string | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. |
ping | string | A space-separated list of URLs to ping when the link is followed. See MDN. |
referrerPolicy | HTMLAttributeReferrerPolicy | How much of the referrer to send when following the link. See MDN. |
routerOptions | RouterOptions | Options for the configured client side router. |
Events
Name | Type | Description |
onHoverStart | (
(e: HoverEvent
)) => void | Handler that is called when a hover interaction starts. |
onHoverEnd | (
(e: HoverEvent
)) => void | Handler that is called when a hover interaction ends. |
onHoverChange | (
(isHovering: boolean
)) => void | Handler that is called when the hover state changes. |
Accessibility
Name | Type | Description |
aria-label | string | An accessibility label for this tree item. |
TreeItemContent#
Name | Type | Description |
children | ReactNode | (
(values: T
& & {}
)) => 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:
Name | CSS Selector | Description |
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. |
state | — | State 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:
Name | CSS Selector | Description |
isExpanded | — | Whether the tree item is expanded. |
isFocusVisibleWithin | — | Whether 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 |
selectionMode | [data-selection-mode="single | multiple"] | The type of selection that is allowed in the collection. |
selectionBehavior | — | The 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);
}