---
title: "Select"
description: "A control for choosing one or more values from a dropdown list."
category: Form
status: beta
since: 0.3.4
tags: [select, form, dropdown]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
:::TuffDemoWrapper{demo="SelectSelectDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxSelectValue } from '@talex-touch/tuffex'
  import { ref, useId } from 'vue'

  const value = ref<TxSelectValue>('engineering')
  const controlId = useId()
  </script>

  <template>
    <label :for="controlId">Team</label>
    <TuffSelect :id="controlId" v-model="value" placeholder="Please select" aria-label="Team">
      <TuffSelectItem value="design" label="Design team" />
      <TuffSelectItem value="engineering" label="Engineering team" />
      <TuffSelectItem value="support" label="Support team" />
    </TuffSelect>
  </template>
---
:::

### Local Filtering
`searchable` renders a search field in the panel that filters registered option labels.
:::TuffDemoWrapper{demo="SelectSelectSearchableDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="value" searchable search-placeholder="Type a label">
      <TuffSelectItem value="docs" label="Docs" />
      <TuffSelectItem value="sdk" label="SDK" />
      <TuffSelectItem value="release" label="Release" />
    </TuffSelect>
  </template>
---
:::

### Remote Search
Pair `remote` with `editable`: the trigger becomes editable and typing emits `search` while the panel is open. Show pending results with `loading`.
:::TuffDemoWrapper{demo="SelectSelectRemoteSearchableDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxSelectOption, TxSelectValue } from '@talex-touch/tuffex'
  import { ref } from 'vue'

  const value = ref<TxSelectValue>('agent-builder')
  const remoteOptions = ref<TxSelectOption[]>([])

  async function onSearch(query: string) {
    remoteOptions.value = await searchApi(query)
  }
  </script>

  <template>
    <TuffSelect v-model="value" remote editable placeholder="Search remote capabilities" @search="onSearch">
      <TuffSelectItem
        v-for="option in remoteOptions"
        :key="option.value"
        :value="option.value"
        :label="option.label"
      />
    </TuffSelect>
  </template>
---
:::

### Multiple Tags
With `multiple`, `v-model` is an array; clicking an option toggles it and keeps the panel open.
:::TuffDemoWrapper{demo="SelectSelectMultipleTagsDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="values" multiple :options="options" :max-tag-count="2" placeholder="Select tags" />
  </template>
---
:::

### Inline Creation
With `allowCreate`, Enter or the footer button emits `create` from the current input and selects the new value.
:::TuffDemoWrapper{demo="SelectSelectAllowCreateDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxSelectOption, TxSelectValue } from '@talex-touch/tuffex'
  import { ref } from 'vue'

  const value = ref<TxSelectValue[]>(['agent'])
  const options = ref<TxSelectOption[]>([
    { value: 'agent', label: 'agent' },
    { value: 'workflow', label: 'workflow' },
  ])

  function onCreate(option: TxSelectOption) {
    options.value = [...options.value, option]
  }
  </script>

  <template>
    <TuffSelect
      v-model="value"
      multiple
      searchable
      allow-create
      create-text="Add tag"
      :options="options"
      @create="onCreate"
    />
  </template>
---
:::

### Groups and Footer
`options` accepts `{ label, options }` groups; the `footer` slot holds add buttons, helper text, or shortcuts.
:::TuffDemoWrapper{demo="SelectSelectGroupedFooterDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  const options = [
    { label: 'Manager', options: [{ value: 'jack', label: 'Jack' }, { value: 'lucy', label: 'Lucy' }] },
    { label: 'Engineer', options: [{ value: 'yiminghe', label: 'Yiminghe' }] },
  ]
  </script>

  <template>
    <TuffSelect v-model="value" :options="options" placeholder="Select member">
      <template #footer>
        <button type="button">+ Add member</button>
      </template>
    </TuffSelect>
  </template>
---
:::

### Icons and Descriptions
Options can carry an `icon` (a class resolved by the host app's styles) and a second-line `description`.
:::TuffDemoWrapper{demo="SelectSelectRichOptionsDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxSelectOptionLike } from '@talex-touch/tuffex'

  const options: TxSelectOptionLike[] = [
    {
      label: 'Recent',
      options: [
        { value: 'design-studio', label: 'Design Studio', icon: 'i-carbon-paint-brush', description: '12 projects' },
        { value: 'product-team', label: 'Product Team', icon: 'i-carbon-layers', description: '8 projects' },
      ],
    },
  ]
  </script>

  <template>
    <TuffSelect v-model="value" searchable :options="options" placeholder="Select workspace" />
  </template>
---
:::

### Status Border
`status` only draws the border; keep validation messages in the form item.
:::TuffDemoWrapper{demo="SelectSelectStatusDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="errorValue" status="error" :options="options" placeholder="Error status" />
    <TuffSelect v-model="warningValue" status="warning" :options="options" placeholder="Warning status" />
  </template>
---
:::

### Disabled
Disable the whole select when the value is read-only; disable a single `TuffSelectItem` to keep an unavailable choice in the list.
:::TuffDemoWrapper{demo="SelectSelectDisabledDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="value" placeholder="Disabled" disabled eager>
      <TuffSelectItem value="locked" label="Locked configuration" />
    </TuffSelect>
  </template>
---
:::

:::TuffDemoWrapper{demo="SelectSelectDisabledOptionDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="value" placeholder="Please select">
      <TuffSelectItem value="standard" label="Standard policy" />
      <TuffSelectItem value="enterprise" label="Enterprise policy" disabled />
      <TuffSelectItem value="manual" label="Manual policy" />
    </TuffSelect>
  </template>
---
:::

### Scrolling Panel
`dropdownMaxHeight` caps the panel height; on open, the selected item scrolls into view.
:::TuffDemoWrapper{demo="SelectSelectScrollableDropdownDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="value" placeholder="Select queue" :dropdown-max-height="180">
      <TuffSelectItem v-for="queue in queues" :key="queue.value" :value="queue.value" :label="queue.label" />
    </TuffSelect>
  </template>
---
:::

### Width
The trigger defaults to 240px and the panel always matches it; set a width on the component to change both.
:::TuffDemoWrapper{demo="SelectSelectWidthDemo" code-lang="vue"}
---
code: |
  <template>
    <TuffSelect v-model="compact" :options="options" />
    <TuffSelect v-model="fluid" style="width: 100%" :options="options" />
  </template>
---
:::

### Best Practices

- Use `options` for data-driven lists and `TuffSelectItem` for a few static items with inline markup; don't mix the two.
- Name a single select with an adjacent `<label for>` and `id`, or `aria-label`; name a multiple select with `aria-label` / `aria-labelledby`. Don't wrap the select in a label.
- Keep remote requests in the host and limit them with `searchDebounce`.
- To show a created option again, add it to your own `options` in the `create` handler.
- Match `modelValue` to `multiple`: a scalar for single, an array for multiple.

## API Reference

### TuffSelect

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `modelValue` / `v-model` | `string \| number \| Array<string \| number>` | `''` | Selected value; an array with `multiple`. |
| `placeholder` | `string` | `'Please select'` | Trigger placeholder when nothing is selected. |
| `disabled` | `boolean` | `false` | Disables the trigger, closes the panel, and blocks selection. |
| `multiple` | `boolean` | `false` | Multiple selection, shown as tags. |
| `status` | `'default' \| 'error' \| 'warning'` | `'default'` | Validation border state. |
| `eager` | `boolean` | `true` | Mounts the panel up front so slot options register reliably. |
| `options` | `TxSelectOptionLike[]` | `[]` | Plain or grouped options. |
| `maxTagCount` | `number` | - | Maximum visible tags; the rest collapse into `+ N ...`. |
| `maxTagTextLength` | `number` | - | Maximum tag text length before truncation. |
| `searchable` | `boolean` | `false` | Shows a local search field in the panel when not editable. |
| `searchPlaceholder` | `string` | `'Search'` | Placeholder of the panel search field. |
| `editable` | `boolean` | `false` | Makes the trigger text editable as the query and opens on focus. |
| `remote` | `boolean` | `false` | Editable remote mode; emits `search` while the panel is open. |
| `allowCreate` | `boolean` | `false` | Allows creating an option from the current input. |
| `createText` | `string` | `'Add item'` | Label of the default create button. |
| `loading` | `boolean` | `false` | Shows the loading state. |
| `loadingText` | `string` | `'Loading...'` | Default loading text. |
| `emptyText` | `string` | `'No results'` | Default empty text. |
| `searchDebounce` | `number` | `0` | Debounce of the remote `search`, in milliseconds. |
| `dropdownMaxHeight` | `number` | `280` | Maximum panel list height in pixels. |
| `dropdownOffset` | `number` | `6` | Distance between the panel and the trigger. |
| `contentPadding` | `number` | `8` | Spacing inside the panel. |
| `optionPadding` | `number` | `0` | Horizontal inset compensation for options. |
| `animation` | `BaseAnchorAnimationOptions` | - | Animation options forwarded to Popover / BaseAnchor. |
| `panelVariant` | `'solid' \| 'dashed' \| 'plain'` | `'solid'` | Panel surface style. |
| `panelBackground` | `'pure' \| 'mask' \| 'blur' \| 'glass' \| 'refraction'` | `'refraction'` | Panel background. |
| `panelShadow` | `'none' \| 'soft' \| 'medium'` | `'soft'` | Panel shadow strength. |
| `panelRadius` | `number` | `18` | Panel corner radius. |
| `panelPadding` | `number` | `0` | Panel padding. |
| `panelCard` | `BaseAnchorPanelCardProps` | - | Card overrides forwarded to the panel. |

#### Events

| Event | Params | Description |
|------|------|------|
| `update:modelValue` | `(value: string \| number \| Array<string \| number>)` | Fires after a selection or clear; an array with `multiple`. |
| `change` | `(value: string \| number \| Array<string \| number>)` | Fires together with `update:modelValue`. |
| `search` | `(query: string)` | In remote mode, fires when the input text changes while the panel is open. |
| `create` | `(option: TxSelectOption)` | Fires when `allowCreate` creates an option from the input. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | - | Options declared with `TuffSelectItem` (legacy form). |
| `option` | `{ option, selected }` | Content of one option in `options` mode. |
| `tag` | `{ option, remove }` | Content of a selected tag. |
| `group` | `{ group }` | Group heading. |
| `loading` | - | Loading state. |
| `empty` | - | Empty state. |
| `footer` | `{ query, canCreate, create }` | Panel footer content. |

#### Exposed Methods

| Method | Description |
|------|------|
| `open()` | Opens the panel. |
| `close()` | Closes the panel. |
| `toggle()` | Toggles the panel. |
| `focus()` | Focuses the trigger input. |
| `blur()` | Blurs the trigger input. |
| `clear()` | Clears the value and selected label; emits `''` in single mode, `[]` in multiple mode. |

### TuffSelectItem

#### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `value` | `string \| number` | - | Value written to the select when picked. |
| `label` | `string` | - | Displayed and selected-state text; defaults to the slot text. |
| `disabled` | `boolean` | `false` | Can't be picked but stays in the list. |
| `icon` | `string` | - | Class of the leading icon, resolved by the host app's styles. |
| `description` | `string` | - | Secondary text under the label. |

#### Slots

| Slot | Props | Description |
|------|------|------|
| `default` | - | Title content of the option row; falls back to `label`, then the value. |

### Types

:::TuffCodeBlock{lang="ts"}
---
code: |
  type TxSelectValue = string | number

  interface TxSelectOption {
    value: TxSelectValue
    label: string
    disabled?: boolean
    icon?: string
    description?: string
  }

  interface TxSelectOptionGroup {
    label: string
    disabled?: boolean
    options: TxSelectOption[]
  }

  type TxSelectOptionLike = TxSelectOption | TxSelectOptionGroup
---
:::

## Overview

- When `options` is set it renders the list, and `TuffSelectItem` registers only without it. A missing `label` falls back to the slot text, then the raw value.
- In non-editable mode, clicking the trigger toggles the panel; with `editable` or `remote`, focus opens it.
- A single select closes after an enabled option is picked; a multiple select stays open.
- `searchable` filters locally only when not editable; `remote` turns local filtering off and emits `search` only while the panel is open.
- A value passed before its options mount gets its label once they register; finite numeric strings and numbers match by numeric value.
- The trigger is a `combobox` (`aria-haspopup="listbox"`, `aria-expanded`), the list a `listbox` (`aria-multiselectable` with `multiple`), and each option an `option` with `aria-selected`. `id` and accessible names land on the actual combobox; `class` and `style` stay on the root.

## Technologies

- The panel renders through Popover; `TuffSelectItem` registers its value and label with the parent through injection.
- Source: `packages/tuffex/packages/components/src/select/`; exports `TuffSelect` and `TuffSelectItem` (aliases `TxSelect`, `TxSelectItem`) and the public types.

<TuffDocSourceLink />
