Styling contract
Headless tree styling relies on slot and state attributes from the primitives. Those hooks are enough to build file explorers, settings panes, and navigation trees without coupling to private markup.
CSS contract table
| Selector | Applied on | Purpose |
|---|---|---|
[data-slot='tree'] | Root controller | Overall tree typography, spacing, and interactive scope. |
[data-slot='tree-item'] | Each node | Row padding, cursor, and node-level affordances. |
[data-slot='tree-group'] | Child branch container | Indentation, connector lines, and nested layout. |
[data-slot='tree-indicator'] | Disclosure affordance | Chevron rotation, icon swap, or branch marker styling. |
[data-expanded='true' | 'false'] | Item and indicator | Branch visibility and indicator-state styling. |
[data-selected='true'] | Selected item | Selection highlight and emphasis treatment. |
[data-disabled='true'] | Disabled item | Muted or blocked presentation for disabled nodes. |
Branch visibility
The primitive reflects branch state through data-expanded, but it does not hide child groups for you. Most headless trees need a rule like [data-slot='tree-item'][data-expanded='false'] > [data-slot='tree-group']{ display: none; } so collapsed branches stop rendering.
State selectors
- Use
:focus-visibleon[data-slot='tree-item']for roving-focus indication. - Style
[data-selected='true']on the node row, not just the indicator, so current selection reads clearly. - Pair
[data-disabled='true']with reduced opacity and a non-interactive cursor. - When visible labels are terse, provide richer
aria-labelvalues because typeahead reads them first.