Customization
TreeSelect is made of four parts, and each one can be styled or replaced:
| Part | Renders | Style with | Replace with |
|---|---|---|---|
TreeNode | The container for a node and its nested nodes | nodeClassName, nodeStyle | CustomNodeComponent |
NodeCheck | The checkbox | inputClassName, inputStyle | CustomCheckComponent |
NodeLabel | The label text | labelClassName, labelStyle | CustomLabelComponent |
ExpandIcon | The arrow next to nodes with children | iconClassName, iconStyle | CustomExpandIconComponent |
className and style on TreeSelect itself apply to the root element.
Class names
Pass class names to any part and they override the defaults. For example, labelClassName='ps-6' replaces the default indentation of the label. You don't need Tailwind to use the component: any class names or style objects work, and the defaults come from the bundled stylesheet.
How the default styles are scoped
The bundled stylesheet is built with Tailwind CSS, but it can't affect the rest of your app:
- Every class is prefixed with
rts:(for examplerts:ps-3), so no default rule can match an element outside the tree. - The rules live in the
componentscascade layer. Rules in a layer lose to Tailwind'sutilitieslayer and to any CSS that isn't in a layer, so your classes and stylesheets override the defaults regardless of specificity or load order. - It contains no global reset (Preflight) and declares no theme variables.
Because the defaults have low priority, unlayered global resets in your app, such as * { margin: 0; padding: 0 }, also override them and can remove the tree's indentation. Scope such resets, or move them into a low-priority layer such as @layer base.
See the styling example.
Replacing parts
Custom components receive the same props as the default they replace, so you can wrap a default part or build your own. Every part receives standardComponentProps, which contains the node being rendered and the property names you passed to TreeSelect.
- Expand icon: see the custom icon example.
- Checkbox: see the file tree example, which replaces checkboxes with folder and file icons.
- Node: see the comment thread example.
The default parts are exported (TreeNode, NodeCheck, NodeLabel, ExpandIcon) so a custom component can render them:
import TreeSelect, { NodeLabel, NodeLabelProps } from '@attebyte/react-tree-select';
const LabelWithCount = (props: NodeLabelProps<ExampleNode>) => (
<>
<NodeLabel {...props} />
<span className='ms-2 text-sm opacity-60'>{props.standardComponentProps.node.children?.length}</span>
</>
);
Custom nodes
A custom node replaces the whole row and controls where nested nodes go. To keep keyboard navigation and screen reader support, it should:
- Spread
treeItemPropsonto its root element. These are therole,tabIndexandaria-*attributes plus a ref that keyboard navigation uses to move focus. - Render
children(the nested nodes, provided while the node is expanded), ideally inside an element withrole='group'. - Use
expandedandonExpandedChangeto show and change whether the node is expanded, andonChange(node, value)to change selection.
import { TreeNodeProps } from '@attebyte/react-tree-select';
const MyNode = (props: TreeNodeProps<ExampleNode>) => {
const { standardComponentProps, treeItemProps, expanded, onExpandedChange, children } = props;
return (
<div {...treeItemProps}>
<button onClick={() => onExpandedChange?.(!expanded)}>
{standardComponentProps.node.label}
</button>
{children && <div role='group'>{children}</div>}
</div>
);
};
If you spread the remaining props onto a DOM element, destructure treeItemProps, expanded, onExpandedChange and children first, as above, so they aren't passed to the DOM.
A custom node that ignores treeItemProps and children and renders its own nested nodes still works, but those nodes aren't part of the tree's keyboard navigation. In that case the default checkbox stays a normal, focusable checkbox.