Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelog App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip
ARC UI ARC Radiant Components
v3.2 Docs Components Tokens Synthesizer
Getting StartedFrameworksServer Rendering Design TokensThemingTheme SynthesizerTypographyUtilities All ComponentsAccessibilityBrowser SupportChangelogContributingStats App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip

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.

Components Tree Select
input interactive
<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

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

items Array<{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.
value string ''
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-values string[] []
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.
placeholder string 'Select...'
Hint text displayed inside the trigger when no leaf is selected. It disappears once a value is chosen.
label string ''
Visible label rendered above the trigger. Also serves as the accessible name. Always provide one for accessibility compliance.
name string ''
Form field name submitted with the selected leaf value via ElementInternals.
disabled boolean false
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.
error string ''
Error message displayed below the trigger. When set, the trigger border turns red.
open boolean false
Controls whether the tree panel is visible. Automatically set to false when a leaf is selected, Escape is pressed, or the user clicks outside.
formAssociated boolean true
properties object { 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).
autoValidates boolean true
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
required boolean false
readonly boolean false

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