For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/en/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/en/components/tree-select.md.

TreeSelect

Choose one or more values from hierarchical data.

Current value: None
basic.tsx
import { Space, Text, TreeSelect } from '@ifhover/leaf-ui';

import { useState } from 'react';

import { treeOptions } from './options';

export function TreeSelectBasic() {
  const [value, setValue] = useState<string | null>(null);
  return (
    <Space direction="vertical" align="stretch">
      <TreeSelect
        options={treeOptions}
        value={value}
        onChange={setValue}
        defaultExpandAll
        aria-label="Team"
        placeholder="Select a team"
      />
      <output>
        <Text tone="muted">Current value: {typeof value === 'string' ? value : 'None'}</Text>
      </output>
    </Space>
  );
}

Multiple selection and checks

Visual design
Visual design
Design
multiple.tsx
import { Form, FormField, TreeSelect } from '@ifhover/leaf-ui';

import { treeOptions } from './options';

export function TreeSelectMultiple() {
  return (
    <Form layout="vertical">
      <FormField label="Multiple · Select individual items">
        <TreeSelect options={treeOptions} multiple defaultValue={['visual']} defaultExpandAll />
      </FormField>
      <FormField label="Linked checks · Child values">
        <TreeSelect
          options={treeOptions}
          treeCheckable
          defaultValue={['visual']}
          defaultExpandAll
        />
      </FormField>
      <FormField label="Linked checks · Parent values">
        <TreeSelect
          options={treeOptions}
          treeCheckable
          showCheckedStrategy="parent"
          defaultValue={['design']}
          defaultExpandAll
        />
      </FormField>
    </Form>
  );
}

States and panel width

states.tsx
import { Form, FormField, TreeSelect } from '@ifhover/leaf-ui';

import { treeOptions } from './options';

export function TreeSelectStates() {
  return (
    <Form layout="vertical">
      <FormField label="Disabled">
        <TreeSelect options={treeOptions} disabled defaultValue="web" />
      </FormField>
      <FormField label="Validation feedback">
        <TreeSelect options={treeOptions} status="error" placeholder="Select a team" />
      </FormField>
      <FormField label="Custom popup width">
        <TreeSelect options={treeOptions} popupWidth={320} popupMaxWidth={360} defaultExpandAll />
      </FormField>
    </Form>
  );
}

Extended features

extensions.tsx
import { TreeSelect } from '@ifhover/leaf-ui';

export function ExtensionDemo() {
  return (
    <TreeSelect
      placeholder="Expand the team to load members"
      options={[{ value: 'team', label: 'Team', isLeaf: false }]}
      virtual
      loadData={async (option) => {
        await new Promise((resolve) => setTimeout(resolve, 500));
        return [
          { value: `${option.value}-design`, label: 'Design team', isLeaf: true },
          { value: `${option.value}-dev`, label: 'Development team', isLeaf: true },
        ];
      }}
    />
  );
}

Selection limits and node rendering

The popup matches the field width by default (popupWidth="trigger"). Set popupWidth="auto" to fit the content, which may be narrower than the field, or popupWidth={300} for a fixed 300px width; CSS lengths are also supported. popupMaxWidth is unset by default. The example below fits the content with an explicit maximum width. Viewport edge avoidance still applies.

Choose up to two people; keep one tag visible
capabilities.tsx
import { Flex, Space, Text, TreeSelect } from '@ifhover/leaf-ui';

import { useState } from 'react';

const options = [
  {
    value: 'team',
    label: 'Team',
    children: [
      { value: 'alice', label: 'Alice' },
      { value: 'bob', label: 'Bob' },
      { value: 'carol', label: 'Carol' },
    ],
  },
];

export function Capabilities() {
  const [expanded, setExpanded] = useState<readonly string[]>(['team']);
  return (
    <Space direction="vertical" align="stretch">
      <Flex direction="column" gap="sm" align="center">
        <Text>
          {'Choose up to two people; keep one tag visible'}
        </Text>
        <TreeSelect
          aria-label={'Team members'}
          popupWidth="auto"
          popupMaxWidth={320}
          treeCheckable
          options={options}
          maxCount={2}
          maxTagCount={1}
          treeExpandedKeys={expanded}
          onExpand={setExpanded}
          nodeRender={(node) => <strong>{node.label}</strong>}
          filterTreeNode={(node, query) =>
            String(node.label).toLowerCase().includes(query.toLowerCase())
          }
          showSearch
        />
      </Flex>
    </Space>
  );
}

Single-line selections

Set tagWrap={false} to keep selected values on one line. Long labels truncate; extra values collapse into +N. In very narrow controls, only the count is shown. Open the popup to review or change selections. maxTagCount can further limit the visible tags; maxTagCount="responsive" also enables automatic single-line collapsing.

Reopening preserves the expanded branches. Click a category that cannot be selected to expand it; in checkbox mode, click anywhere on a row or press Enter / Space to check it. The arrow only expands or collapses the branch. Search temporarily reveals matching branches without changing the saved expansion.

Visual designProduct designFrontendBackend
single-line.tsx
import { Form, FormField, TreeSelect } from '@ifhover/leaf-ui';

import { treeOptions } from './options';

export function SingleLine() {
  return (
    <Form layout="vertical">
      <FormField label="Multiple values on one line">
        <TreeSelect
          treeCheckable
          tagWrap={false}
          options={treeOptions}
          defaultValue={['visual', 'product', 'web', 'server']}
          treeDefaultExpandedKeys={['design']}
        />
      </FormField>
    </Form>
  );
}

TreeSelect API

PropertyTypeDefaultDescription
popupPlacement'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end'—Placement relative to the trigger.
popupClassNamestring—Custom class for the popup.
popupStyleCSSProperties—Custom popup styles.
popupRender(content: React.ReactNode) => React.ReactNode—Wrap or extend popup content; retain the supplied content to preserve interaction.
getPopupContainer() => Element | DocumentFragment—Return a popup container, including a ShadowRoot.
optionsreadonly TreeSelectOption[]RequiredRequired. Hierarchical options
checkStrictlybooleanfalseCheck parents and children independently
showCheckedStrategy'child' | 'parent' | 'all''child'Display and output children, consolidated parents or all checked nodes
showSearchbooleantrueFilter nodes by typing
onSearch(query: string) => void—Search changes
allowClearbooleantrueAllow clearing enabled selections
defaultExpandAllbooleanfalseExpand all initially
treeDefaultExpandedKeysreadonly string[][]Values expanded initially
size'sm' | 'md' | 'lg''md'Control size
status'error' | 'warning'—Validation status
popupWidthnumber | string'trigger'Popup width: trigger matches the field, auto fits content (and may be narrower), numbers specify fixed pixels; CSS lengths are also supported
popupMaxWidthnumber | string—Optional maximum width; unset by default, still bounded by the viewport
notFoundContentReactNodeNo optionsContent when no options match
onOpenChange(open: boolean) => void—Open state changes
loadData( option: TreeSelectOption, signal: AbortSignal, ) => Promise<readonly TreeSelectOption[]>—Load children on expansion; signal supports cancellation
onLoadError(error: unknown, option: TreeSelectOption) => void—Loading failed; expand again to retry
virtualbooleanfalseRender visible items only
listHeightnumber256List height
itemHeightnumber38Estimated virtual row height including spacing; follows the regional height and density. Actual rows are measured.
inputReadOnlyboolean—Disable typing while retaining panel selection.
openboolean—Control whether the panel is open.
defaultOpenboolean—Initial open state.
maxTagCountnumber | 'responsive'—Maximum visible tags; responsive collapses them to the available width.
tagWrapbooleantrueAllow selected tags to wrap; false keeps one line and collapses overflow.
maxCountnumber—Maximum selection count.
treeExpandedKeysreadonly string[]—Controlled expanded nodes.
onExpand(keys: string[]) => void—Called when expanded nodes change.
filterTreeNode(option: TreeSelectOption, query: string) => boolean—Customize search matching for nodes.
nodeRender(option: TreeSelectOption) => ReactNode—Customize node content.
cacheKeystring | number—Changing the data version clears lazy caches and cancels stale requests.
multiplefalse | true | booleanfalseMultiple / checkbox mode, which automatically enables multiple selection
treeCheckablefalse | boolean | truefalseMultiple / checkbox mode, which automatically enables multiple selection
valuestring | null | readonly string[] | readonly string[]nullControlled / initial value; use an array for multiple selection
defaultValuestring | null | readonly string[] | readonly string[]nullControlled / initial value; use an array for multiple selection
onChange(((value: string | null) => void) | ((value: readonly string[]) => void)) | ((value: readonly string[]) => void)—Selection changes; clearing returns null for single mode or [] for multiple
disabledbooleanfalseDisable interaction.
requiredbooleanfalseRequire a non-empty value; compound controls validate once.
namestring—Field name in native FormData.
formstring—ID of the associated native form.

TreeSelectOption

PropertyTypeDefaultDescription
valuestring—Required. Unique value, required
labelReactNode—Required. Display label, required
childrenreadonly TreeSelectOption[]—Child options using the same structure
disabledboolean—Disable interaction.
selectableboolean—Set false to make a category non-selectable
disableCheckboxboolean—Disable checks and stop propagation
iconReactNode—Node icon
searchLabelstring—Search and accessible text for non-text labels
isLeafboolean—Identify a leaf node, including in lazy data.

Types

Import these types to define props or prepare data.

import type { TreeSelectProps, TreeSelectOption, TreeSelectValue } from '@ifhover/leaf-ui';

Usage notes

Option values must be unique and stable. Example data:

import type { TreeSelectOption } from '@ifhover/leaf-ui';
export const treeOptions: readonly TreeSelectOption[] = [
  {
    value: 'design',
    label: 'Design',
    selectable: false,
    children: [
      { value: 'visual', label: 'Visual design' },
      { value: 'product', label: 'Product design' },
      { value: 'archive', label: 'Archived team', disabled: true },
    ],
  },
  {
    value: 'engineering',
    label: 'Engineering',
    selectable: false,
    children: [
      { value: 'web', label: 'Frontend' },
      { value: 'server', label: 'Backend' },
    ],
  },
];

TreeSelectValue is string | readonly string[] | null and can be used for controlled state. Checkbox mode submits checked leaves by default; showCheckedStrategy controls both display and callback values. Disabled existing selections are preserved by clear and linked checks.

ArrowDown enters the tree, Enter selects, Space checks and Escape closes. With an empty search in multiple mode, Backspace removes the last enabled selection. FormData submits repeated values under one field name; use getAll(name) to read them. Uncontrolled fields respond to form reset.