Skip to main content

Customization

TreeSelect is made of four parts, and each one can be styled or replaced:

PartRendersStyle withReplace with
TreeNodeThe container for a node and its nested nodesnodeClassName, nodeStyleCustomNodeComponent
NodeCheckThe checkboxinputClassName, inputStyleCustomCheckComponent
NodeLabelThe label textlabelClassName, labelStyleCustomLabelComponent
ExpandIconThe arrow next to nodes with childreniconClassName, iconStyleCustomExpandIconComponent

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 example rts:ps-3), so no default rule can match an element outside the tree.
  • The rules live in the components cascade layer. Rules in a layer lose to Tailwind's utilities layer 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.

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:

  1. Spread treeItemProps onto its root element. These are the role, tabIndex and aria-* attributes plus a ref that keyboard navigation uses to move focus.
  2. Render children (the nested nodes, provided while the node is expanded), ideally inside an element with role='group'.
  3. Use expanded and onExpandedChange to show and change whether the node is expanded, and onChange(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.