Tree Select
Dropdown select whose panel is a hierarchical tree — categories, instrument banks, folder pickers. Group nodes expand and collapse; only leaf nodes are selectable.
<arc-tree-select> Overview
>
Tree Select combines the trigger anatomy of Select with a hierarchical tree panel. Instead of a flat list of options, the dropdown presents expandable groups whose leaves are the actual choices: an instrument bank organized by family, a category taxonomy, a folder structure. The trigger shows the chosen leaf together with a muted breadcrumb of its ancestor path, so "Violin" reads as "Strings / Violin" and never loses its context.
Selection is leaf-only by design. Nodes with children act as group headers — they expand and collapse but can never be chosen — which keeps single-select semantics clean: the submitted value is always one unambiguous leaf, never a branch that might mean "everything under it". Branches containing the current value expand automatically when the panel opens, so the selection is always visible without hunting.
Tree Select implements the ARIA combobox pattern with a tree popup. Keyboard users open the panel with Enter, Space, or an arrow key, walk rows with Arrow Up and Down, expand and collapse groups with Arrow Right and Left, confirm a leaf with Enter, and dismiss with Escape. Typing jumps to the row starting with those letters, exactly as in Select. The component participates in native forms through ElementInternals, submitting the selected leaf value under its `name`.Guidelines
When to use
- Use Tree Select when the options have a real hierarchy the user thinks in — instrument families, product categories, folder trees
- Give every node a stable value, including group headers — group values drive expanded-values and appear in the arc-change path detail
- Keep the tree shallow; two or three levels is comfortable inside a dropdown panel
- Pre-expand the branches users need most via expanded-values instead of making them dig
- Always provide a visible label so users understand what they are choosing
- Use disabled nodes for temporarily unavailable choices rather than removing them, so the structure stays recognizable
When not to use
- Do not use Tree Select for a flat list — use Select, which is simpler for both hands and screen readers
- Do not use it when users need to type to filter a large set — use Combobox, whose text field owns the keystrokes
- Do not use it for browsing or navigation outside a form — use Tree View, which is a standalone tree without a trigger or form value
- Do not expect group headers to be selectable — if a branch itself must be a valid choice, add an explicit leaf such as "All Strings" inside it
- Do not nest deeper than three levels — a dropdown panel is the wrong home for a deep tree; consider a dedicated picker dialog instead
Features
- Hierarchical tree panel with expandable, collapsible group headers
- Leaf-only selection keeps single-select semantics unambiguous
- Trigger breadcrumb shows the ancestor path muted beside the leaf label
- Branches containing the selected value auto-expand when the panel opens
- Initially expanded branches via the expanded-values property
- Full keyboard support: arrows navigate and expand, Enter selects, Escape closes
- Type-ahead jumps to rows by their first letters, as in a native select
- Disabled nodes render but are skipped by keyboard and cannot be selected
- Native form participation via ElementInternals, including required validation
- Neutral depth rails mark nesting structure without carrying state
Preview
Instrument
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<script type="module" src="@arclux/arc-ui"></script>
<arc-tree-select
label="Instrument"
name="instrument"
placeholder="Pick an instrument..."
></arc-tree-select>
<script>
const treeSelect = document.querySelector('arc-tree-select');
// items is a property, not an attribute: assign the tree from script.
treeSelect.items = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
{ value: 'percussion', label: 'Percussion', children: [
{ value: 'timpani', label: 'Timpani' },
] },
];
// Pre-expand a branch, or let auto-expand follow the selected value.
treeSelect.expandedValues = ['strings'];
treeSelect.addEventListener('arc-change', (e) => {
// e.detail.path holds the ancestor group values, root first.
console.log('Selected:', e.detail.value, 'in', e.detail.path.join(' / '));
});
</script> import { TreeSelect } from '@arclux/arc-ui-react';
import { useState } from 'react';
const instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
{ value: 'percussion', label: 'Percussion', children: [
{ value: 'timpani', label: 'Timpani' },
] },
];
function InstrumentPicker() {
const [instrument, setInstrument] = useState('');
return (
<TreeSelect
label="Instrument"
placeholder="Pick an instrument..."
items={instruments}
value={instrument}
onArcChange={(e) => setInstrument(e.detail.value)}
/>
);
} <script setup>
import { TreeSelect } from '@arclux/arc-ui-vue';
import { ref } from 'vue';
const instrument = ref('');
const instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
];
</script>
<template>
<TreeSelect
label="Instrument"
placeholder="Pick an instrument..."
:items="instruments"
:value="instrument"
@arc-change="instrument = $event.detail.value"
/>
</template> <script>
import { TreeSelect } from '@arclux/arc-ui-svelte';
let instrument = '';
const instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
];
</script>
<TreeSelect label="Instrument" placeholder="Pick an instrument..."
items={instruments} value={instrument}
on:arc-change={(e) => instrument = e.detail.value} /> import { Component } from '@angular/core';
import { TreeSelect } from '@arclux/arc-ui-angular';
@Component({
imports: [TreeSelect],
template: `
<arc-tree-select label="Instrument" placeholder="Pick an instrument..."
[items]="instruments" [value]="instrument"
(arc-change)="instrument = $event.detail.value">
</arc-tree-select>
`,
})
export class InstrumentPickerComponent {
instrument = '';
instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
];
} import { TreeSelect } from '@arclux/arc-ui-solid';
import { createSignal } from 'solid-js';
const instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
];
function InstrumentPicker() {
const [instrument, setInstrument] = createSignal('');
return (
<TreeSelect label="Instrument" placeholder="Pick an instrument..."
items={instruments} value={instrument()}
onArcChange={(e) => setInstrument(e.detail.value)} />
);
} import { TreeSelect } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';
const instruments = [
{ value: 'keys', label: 'Keys', children: [
{ value: 'grand-piano', label: 'Grand Piano' },
{ value: 'rhodes', label: 'Rhodes' },
] },
{ value: 'strings', label: 'Strings', children: [
{ value: 'violin', label: 'Violin' },
{ value: 'cello', label: 'Cello' },
] },
];
function InstrumentPicker() {
const [instrument, setInstrument] = useState('');
return (
<TreeSelect label="Instrument" placeholder="Pick an instrument..."
items={instruments} value={instrument}
onArcChange={(e) => setInstrument(e.detail.value)} />
);
} API
-
itemsArray<{value: string, label: string, children?: Array<object>, disabled?: boolean}>[] - Recursive tree of nodes. A node with a non-empty `children` array is a group header: it expands and collapses but can never be selected. A node without children is a selectable leaf. `disabled` nodes render but cannot be reached by keyboard or selected, and a disabled group hides its children.
-
valuestring'' - The selected leaf's value. Setting it programmatically updates the trigger's breadcrumb label, and the branches containing it auto-expand the next time the panel opens.
-
expanded-valuesstring[][] - Values of group nodes to render initially expanded. Attribute: `expanded-values` (JSON array). Branches containing the selected value auto-expand on open regardless of this list.
-
placeholderstring'Select...' - Hint text displayed inside the trigger when no leaf is selected. It disappears once a value is chosen.
-
labelstring'' - Visible label rendered above the trigger. Also serves as the accessible name. Always provide one for accessibility compliance.
-
namestring'' - Form field name submitted with the selected leaf value via ElementInternals.
-
disabledbooleanfalse - When true, the trigger becomes non-interactive: it cannot be opened, focused, or clicked, and renders with reduced opacity.
-
size'sm' | 'md' | 'lg''md' - Controls the trigger size.
-
errorstring'' - Error message displayed below the trigger. When set, the trigger border turns red.
-
openbooleanfalse - Controls whether the tree panel is visible. Automatically set to false when a leaf is selected, Escape is pressed, or the user clicks outside.
-
formAssociatedbooleantrue -
propertiesobject{ required: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, } - Lit merges static properties up the prototype chain, so every consumer gets these without declaring them. `required` participates in constraint validation below; `readonly` reflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state).
-
autoValidatesbooleantrue - Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
-
form -
validity -
validationMessage -
requiredbooleanfalse -
readonlybooleanfalse
Events
-
arc-change - Fired when a leaf is selected. `detail.value` is the leaf value, `detail.label` its label, and `detail.path` the array of ancestor group values from root to parent.
See Also
- Select Dropdown select with searchable options, keyboard navigation, and full ARIA listbox semantics for accessible form inputs.
- Combobox Searchable dropdown with type-ahead filtering.
- Tree View Hierarchical tree structure with expandable/collapsible nodes, selection tracking, keyboard navigation, and indentation guide lines.
- Multi Select Multi-value select with tag chips, inline search filtering, and keyboard navigation.