dify/packages/dify-ui/docs/selection.md

6.6 KiB

Selection

Choose a selection primitive from the value and interaction contract, then preserve the caller's domain value type through its public API.

Primitive choice

  • RadioGroup selects one persistent field value from a visible set of options. Every Radio or RadioItem belongs to a group; do not render a standalone radio.
  • SegmentedControl selects one mode, filter, or view. It follows radio-group semantics: an active item cannot be toggled off, Tab enters on the selected item, and arrow keys move and select.
  • Tabs selects a panel and provides tablist and tabpanel semantics.
  • Autocomplete accepts free-form text with optional suggestions.
  • Combobox selects and remembers one or more values from a searchable collection.
  • Select chooses from a closed, scannable list without text entry.

Multiple-selection comboboxes follow the Base UI chips composition: chips and the input share the input group, chips wrap, and the group grows vertically.

For chip-based multiple comboboxes, render ComboboxValue around ComboboxChips and label the chips container only while it has the conditional toolbar role. Consumers must localize the ComboboxChip Backspace/Delete description, the item-specific ComboboxChipRemove name, and the input's selection count and Left Arrow hint. If FieldDescription already sets aria-describedby, put the input hint there because it takes precedence over aria-description.

Autocomplete, Combobox, and Select popups use Base UI's --anchor-width and --available-width variables to follow their trigger while clamping to the viewport. Do not replace that sizing with a fixed width or an unclamped minimum width.

Use Radio for the default radio appearance. Use RadioItem when custom content itself is a radio item, and put RadioControl inside it for Dify UI's visual indicator. RadioControl is a visual part, not a standalone radio.

Import the complete family from its single public subpath:

import { Radio, RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio-group'

Typed values

Do not widen domain values to string. Use Select<Value, Multiple>, RadioGroup<Value>, Radio<Value>, and RadioItem<Value> for enums, unions, booleans, numbers, objects, or nullable placeholder values.

Root generics type value, defaultValue, and value-dependent callbacks. JSX children do not inherit the parent's generic, so type independently consumed anatomy when its value cannot be inferred locally:

<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
  <Radio<PromptMode> value={PROMPT_MODE.default} />
  <RadioItem<PromptMode> value={PROMPT_MODE.custom}>
    <RadioControl />
    Custom prompt
  </RadioItem>
</RadioGroup>

For Select and Combobox, the literal multiple-value type must match runtime mode: <Combobox<Subject, true> multiple>. Value display can still receive null before selection. Repeat the domain type on independently consumed render anatomy rather than annotating callback parameters:

<Combobox<Subject, true> multiple value={subjects} onValueChange={setSubjects}>
  <ComboboxValue<Subject, true>>
    {(selected) => selected?.map((subject) => subject.name).join(', ') ?? 'Anyone'}
  </ComboboxValue>
  <ComboboxList<Subject>>
    {(subject) => <ComboboxItem value={subject}>{subject.name}</ComboboxItem>}
  </ComboboxList>
</Combobox>

AutocompleteList follows the same rule. Groups may infer their local type from items; a nested Collection is a separate JSX boundary. Dynamic multiple={condition} produces the corresponding single-or-multiple union.

Prefer the Base UI items collection pattern so the root, value display, and item list share one runtime source of truth. Convert values to strings only at real serialization boundaries.

Combobox source items and selected values

Combobox has separate types for the selected business value and the source record rendered by the list:

import {
  Combobox,
  ComboboxItem,
  ComboboxList,
  createComboboxItems,
} from '@langgenius/dify-ui/combobox'
import { useMemo } from 'react'

const userItems = useMemo(
  () =>
    createComboboxItems(users, {
      getValue: user => user.id,
      getLabel: user => user.name,
    }),
  [users],
)

<Combobox<string, true, User>
  multiple
  items={userItems}
  value={selectedUserIds}
  onValueChange={setSelectedUserIds}
>
  <ComboboxList<User>>
    {user => <ComboboxItem<string> value={user.id}>{user.name}</ComboboxItem>}
  </ComboboxList>
</Combobox>

The first generic is Value, the second is the literal multiple-selection mode, and the third is the source Item. ComboboxValue and ComboboxItem use Value; filter, ComboboxList, ComboboxGroup, ComboboxCollection, and useComboboxFilteredItems use source items. Grouped roots use the leaf record as Item, while the list callback receives the group object.

Use createComboboxItems when the business contract stores a stable primitive ID but list rows need complete records. Its getValue result must be unique and stable, and getLabel owns default filtering, typeahead, and selected-value display. Create static collections at module scope and memoize collections derived from changing data. Treat the returned collection as opaque and pass it directly to items.

For server-side search, keep the complete set of records needed to resolve selected labels in the collection passed to items, and pass the current result window to filteredItems. Filtered items are source Item records, not derived IDs, and grouped results must retain the collection's group shape.

Keep object values when the selected record itself is the business state or the selection callback immediately needs the full record. When async refreshes may replace object references, provide isItemEqualToValue using the stable domain identity.

itemToStringValue only serializes a selected Value for forms and autofill; it does not change onValueChange into an ID callback. Do not add it, itemToStringLabel, or a comparator as a mechanical trio. Primitive IDs normally use the default equality. For async or paged data, keep selected records in the collection when their labels must remain available after they leave the current result window, or provide an ID-only label fallback.

CheckboxGroup follows Base UI and uses string[]. Model stronger business ID distinctions at the domain boundary unless the upstream primitive contract changes.