Add data-tree to a select whose options
nest, and people open branches to choose leaves. Add multiple
to choose several leaves independently.
The options stay flat in the markup. A child names its parent option's value in
data-parent; an option without one is a root, and an option another
option names is a branch. AutoInit() starts every
select[data-tree] as a tree select, with the same field, labels and
validation messages as a select. The native select still holds the value the form
submits.
<div class="field">
<select id="place" name="place" data-tree>
<option value="" disabled selected>Choose a place</option>
<option value="europe" disabled>Europe</option>
<option value="france" data-parent="europe" disabled>France</option>
<option value="paris" data-parent="france">Paris</option>
<option value="lyon" data-parent="france">Lyon</option>
<option value="spain" data-parent="europe">Spain</option>
<option value="asia" disabled>Asia</option>
<option value="tokyo" data-parent="asia">Tokyo</option>
</select>
<label for="place">Place</label>
</div>
Branches and the form
Only a leaf can be chosen. While the tree select runs, it disables every branch
option, so the form never submits a branch, and it restores them when it is
destroyed. In single mode, if nothing is selected, the browser picks the first option,
often a branch; the tree select then chooses the first enabled leaf, as a form
reset would. Multiple mode starts empty unless leaves are selected; it clears
branch selections without choosing any descendants. Put disabled on branch
options so the page works without JavaScript. In single mode, also start
with a value="" placeholder. Multiple selects need no selected placeholder.
A form reset, a required attribute and a disabled select work as on a
select. To disable part of the tree, disable each leaf in it.
Choose several leaves
Add multiple to the native select. Click, Enter or Space toggles
only the focused leaf, without a modifier. Branches only expand or collapse;
choosing a branch never selects its descendants. The popup stays open.
<div class="field">
<select id="many-places" name="places" multiple data-tree
data-tree-select-options='{"searchable":true}'>
<option value="europe" disabled>Europe</option>
<option value="paris" data-parent="europe" selected>Paris</option>
<option value="lyon" data-parent="europe">Lyon</option>
<option value="asia" disabled>Asia</option>
<option value="tokyo" data-parent="asia" selected>Tokyo</option>
</select>
<label for="many-places">Places</label>
</div>
The actual named tree exposes aria-multiselectable="true".
Leaf selection and keyboard focus are separate, with no generated checkboxes.
The trigger and getSelectedValues() list leaves in native option
order. new FormData(form).getAll('places') includes both selected
leaves, even when a query hides one, and never includes a branch.
Each toggle fires one native change. Search, focus, branch
navigation and refresh do not. Required multiple trees with no selected leaf
stay empty and invalid. Reset restores authored leaf defaults, clears search
and closes; a canceled reset preserves the live choices. Branch defaults
never propagate to leaves. Destroy and reinitialize after changing the
native multiple attribute.
Search leaves
Set searchable: true through data-tree-select-options
or TreeSelect.init() to search a single or multiple tree. Search matches
a literal substring of leaf text, ignoring case and trimming query edges.
It retains and opens every matching leaf's ancestors. Branch labels and option
values do not match; disabled matches stay visible and unavailable.
<div class="field">
<select id="search-place" name="place" data-tree
data-tree-select-options='{"searchable":true}'>
<option value="" disabled selected>Choose a place</option>
<option value="europe" disabled>Europe</option>
<option value="france" data-parent="europe" disabled>France</option>
<option value="paris" data-parent="france">Paris</option>
<option value="lyon" data-parent="france">Lyon</option>
<option value="asia" disabled>Asia</option>
<option value="tokyo" data-parent="asia">Tokyo</option>
</select>
<label for="search-place">Search places</label>
</div>
The read-only trigger still shows the chosen leaf. Its named nonmodal dialog
contains a labelled search field, polite no-results status and the named tree.
Search has no form name. Typing, clearing and hiding the chosen leaf preserve
its native value and fire no change. An enabled leaf pick changes
the value once. In single mode it closes and focuses the trigger; in multiple
mode it keeps the popup, query and row focus.
The first nonempty query saves the person's expansion state. Further edits, temporary branch toggles and refresh do not replace it. Clearing or closing restores surviving branches to that state; newly added branches start closed. A matching path opens again on the next query edit or refresh.
| Key or action | Searchable tree behavior |
|---|---|
| Open | Focus search by default. Opening completion preserves focus already inside the popup. |
| Type, paste, Space, Home / End, horizontal keys or composition in search | Edit text normally. Enter neither selects nor submits. |
| ArrowDown / ArrowUp from search | Focus the first / last enabled visible tree row. Stay in search when none exists. |
| Keys on a tree row | Use the tree controls below, including RTL arrows, typeahead and branch toggles. Hidden rows and disabled leaves are skipped. |
| Tab / Shift+Tab | Tab enters the first enabled row from search. From a row, Tab closes and advances after the trigger. Without enabled rows, Tab exits search. Shift+Tab returns from a row to search, or closes from search to the trigger. |
| Escape / outside click | Close and clear search. Escape focuses the trigger; outside dismissal preserves its target's focus. |
refresh() reapplies an active query while preserving the original
expansion state and search caret, or focus on a surviving enabled visible row.
When that row is unavailable, focus moves to an enabled visible row or search.
A successful native reset closes, clears and restores defaults; cancellation
preserves the query, selection and expansion. Disabling the select or its
fieldset and refreshing closes it. Existing menu callbacks and
autoFocus: false work as on a select.
Expressive.TreeSelect.init(select, {
searchable: true,
i18n: { searchLabel: 'Find a leaf', noResults: 'No matching leaves' }
});
Search defaults off and works with single or multiple trees. Browser-default selects stay native. Search does not fold accents, interpret regular expressions, fetch children or change the native options.
Keyboard and pointer
The popup is an ARIA tree. Down and Up move between visible rows without wrapping,
and skip disabled leaves. Right opens a closed branch or moves into an open one;
Left closes an open branch or moves to its parent. Home and End go to the first and
last visible rows, * opens every branch beside the focused row, and
typing a few letters moves to the next row that starts with them. Enter and Space
choose a leaf or toggle a branch. In multiple mode, leaf activation toggles
only that leaf and keeps the popup open. Focus movement never changes choices. Escape closes the popup and returns focus to the
field. Without search, Tab or Shift+Tab closes back to the trigger; the
following Tab moves normally. Under dir="rtl" Left and Right swap.
A click on a branch opens or closes it and keeps the popup open. A click on a leaf
chooses it and fires one change event when the value changed.
Options and methods
A tree select takes the select's classes, menuOptions,
searchable and search i18n,
through script. data-tree-select-options accepts classes,
searchable and existing i18n keys; pass nested Menu options such as
autoFocus through script. Call
refresh() after changing the options; open branches stay open.
const place = Expressive.TreeSelect.getInstance(document.getElementById('place'));
place.refresh(); // rebuild after adding, removing or changing options
place.getSelectedValues(); // ['paris']
place.destroy(); // restores the branch options and the native select
What it does not do
There is no choosable branch, nested
<optgroup>, option image or children loaded on demand. The field
shows chosen leaf text, not ancestor paths.