Select
Dropdown picker — single/multi, searchable, grouped, async loading, keyboard nav, tag overflow.
Basic usage
Pass an options array; each item needs at least value and label.
已选:shanghai
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
];
const city = ref<string | null>('shanghai');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="city" :options="options" placeholder="选一个城市" />
<p class="demo-hint">已选:<code>{{ city ?? 'null' }}</code></p>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
];
const city = ref<string | null>('shanghai');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="city" :options="options" placeholder="选一个城市" />
<p class="demo-hint">已选:<code>{{ city ?? 'null' }}</code></p>
</div>
</template> import { useState } from 'react';
import { CfSelect, type SelectOption } from '@chufix-design/react';
const options: SelectOption[] = [
{ value: 'beijing', label: 'Beijing' },
{ value: 'shanghai', label: 'Shanghai' },
{ value: 'guangzhou', label: 'Guangzhou' },
];
export default function Demo() {
const [city, setCity] = useState<string | null>('shanghai');
return (
<CfSelect
value={city}
options={options}
placeholder="Pick a city"
onChange={(v) => setCity(v as string | null)}
/>
);
} import { useState } from 'react';
import { CfSelect } from '@chufix-design/react';
const options= [
{ value: 'beijing', label: 'Beijing' },
{ value: 'shanghai', label: 'Shanghai' },
{ value: 'guangzhou', label: 'Guangzhou' },
];
export default function Demo() {
const [city, setCity] = useState<string | null>('shanghai');
return (
<CfSelect
value={city}
options={options}
placeholder="Pick a city"
onChange={(v) => setCity(v)}
/>
);
} Variants
Three variants matching Input: outline (default) / filled / ghost.
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'a', label: '选项 A' },
{ value: 'b', label: '选项 B' },
{ value: 'c', label: '选项 C' },
];
const v1 = ref<string | null>('a');
const v2 = ref<string | null>('b');
const v3 = ref<string | null>('c');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="v1" :options="options" variant="outline" />
<CfSelect v-model="v2" :options="options" variant="filled" />
<CfSelect v-model="v3" :options="options" variant="ghost" />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'a', label: '选项 A' },
{ value: 'b', label: '选项 B' },
{ value: 'c', label: '选项 C' },
];
const v1 = ref<string | null>('a');
const v2 = ref<string | null>('b');
const v3 = ref<string | null>('c');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="v1" :options="options" variant="outline" />
<CfSelect v-model="v2" :options="options" variant="filled" />
<CfSelect v-model="v3" :options="options" variant="ghost" />
</div>
</template> <CfSelect value={v1} options={options} variant="outline" onChange={setV1} />
<CfSelect value={v2} options={options} variant="filled" onChange={setV2} />
<CfSelect value={v3} options={options} variant="ghost" onChange={setV3} /> <CfSelect value={v1} options={options} variant="outline" onChange={setV1} />
<CfSelect value={v2} options={options} variant="filled" onChange={setV2} />
<CfSelect value={v3} options={options} variant="ghost" onChange={setV3} /> States and clearable
clearable shows × when a value is selected; disabled disables the whole control; error switches the border to the error color. Individual options can have disabled: true to be skipped.
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'chengdu', label: '成都(暂不可选)', disabled: true },
];
const a = ref<string | null>('shanghai');
const b = ref<string | null>(null);
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="a" :options="options" placeholder="可清空(输入后出现 ×)" clearable />
<CfSelect v-model="b" :options="options" placeholder="禁用" disabled />
<CfSelect v-model="b" :options="options" placeholder="错误状态" error />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'chengdu', label: '成都(暂不可选)', disabled: true },
];
const a = ref<string | null>('shanghai');
const b = ref<string | null>(null);
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="a" :options="options" placeholder="可清空(输入后出现 ×)" clearable />
<CfSelect v-model="b" :options="options" placeholder="禁用" disabled />
<CfSelect v-model="b" :options="options" placeholder="错误状态" error />
</div>
</template> <CfSelect value={a} options={options} clearable onChange={setA} />
<CfSelect value={b} options={options} disabled />
<CfSelect value={b} options={options} error /> <CfSelect value={a} options={options} clearable onChange={setA} />
<CfSelect value={b} options={options} disabled />
<CfSelect value={b} options={options} error /> Events and forms
Beyond value changes, Select exposes events for open state, active option, clear, and focus. When you pass name, it syncs a hidden input so the current value is submitted with native forms.
等待交互:打开菜单、移动 active、选择或清空。<script setup lang="ts">
import { ref } from 'vue';
import {
CfBadge,
CfSelect,
type SelectChangeMeta,
type SelectOption,
type SelectValue,
} from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'api', label: 'API 事件' },
{ value: 'audit', label: '审计筛选' },
{ value: 'release', label: '发布分组' },
{ value: 'locked', label: '锁定项(不可选)', disabled: true },
];
const value = ref<SelectValue>('api');
const active = ref('api');
const logs = ref(['等待交互:打开菜单、移动 active、选择或清空。']);
function record(name: string, detail: string) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 4);
}
function onChange(next: SelectValue, meta: SelectChangeMeta) {
value.value = next;
record('change', `${String(next ?? 'null')} / ${meta.option?.label ?? '无选项'}`);
}
function onSelect(option: SelectOption) {
active.value = String(option.value ?? '');
record('select', option.label);
}
function onActiveChange(option: SelectOption | null, index: number) {
active.value = option ? String(option.value) : 'none';
record('active-change', `${index} / ${option?.label ?? '无'}`);
}
</script>
<template>
<div class="select-events">
<CfSelect
:model-value="value"
:options="options"
placeholder="选择记录类型"
clearable
name="select-event-demo"
@change="onChange"
@select="onSelect"
@clear="record('clear', 'value 已重置为 null')"
@open-change="(open) => record('open-change', open ? 'opened' : 'closed')"
@active-change="onActiveChange"
@focus="record('focus', 'trigger focused')"
@blur="record('blur', 'trigger blurred')"
/>
<div class="select-events__status">
<CfBadge tone="info" :content="active || 'none'" />
<div class="select-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.select-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.select-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.select-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.select-events__log code {
white-space: normal;
}
</style> <script setup>
import { ref } from 'vue';
import {
CfBadge,
CfSelect,
} from '@chufix-design/vue';
const options= [
{ value: 'api', label: 'API 事件' },
{ value: 'audit', label: '审计筛选' },
{ value: 'release', label: '发布分组' },
{ value: 'locked', label: '锁定项(不可选)', disabled: true },
];
const value = ref<SelectValue>('api');
const active = ref('api');
const logs = ref(['等待交互:打开菜单、移动 active、选择或清空。']);
function record(name, detail) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 4);
}
function onChange(next, meta) {
value.value = next;
record('change', `${String(next ?? 'null')} / ${meta.option?.label ?? '无选项'}`);
}
function onSelect(option) {
active.value = String(option.value ?? '');
record('select', option.label);
}
function onActiveChange(option, index) {
active.value = option ? String(option.value) : 'none';
record('active-change', `${index} / ${option?.label ?? '无'}`);
}
</script>
<template>
<div class="select-events">
<CfSelect
:model-value="value"
:options="options"
placeholder="选择记录类型"
clearable
name="select-event-demo"
@change="onChange"
@select="onSelect"
@clear="record('clear', 'value 已重置为 null')"
@open-change="(open) => record('open-change', open ? 'opened' : 'closed')"
@active-change="onActiveChange"
@focus="record('focus', 'trigger focused')"
@blur="record('blur', 'trigger blurred')"
/>
<div class="select-events__status">
<CfBadge tone="info" :content="active || 'none'" />
<div class="select-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.select-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.select-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.select-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.select-events__log code {
white-space: normal;
}
</style> <CfSelect
value={value}
options={options}
clearable
name="status"
onChange={(value, meta) => console.log(value, meta.option)}
onSelect={(option) => console.log('select', option)}
onClear={() => console.log('clear')}
onOpenChange={(open) => console.log('open', open)}
onActiveChange={(option, index) => console.log(option, index)}
/> <CfSelect
value={value}
options={options}
clearable
name="status"
onChange={(value, meta) => console.log(value, meta.option)}
onSelect={(option) => console.log('select', option)}
onClear={() => console.log('clear')}
onOpenChange={(open) => console.log('open', open)}
onActiveChange={(option, index) => console.log(option, index)}
/> Multiple
multiple turns the model into an array; the trigger renders removable tag chips. maxTagCount collapses overflow into a +N pill. Backspace removes the last tag.
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'go', label: 'Go' },
{ value: 'rs', label: 'Rust' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
];
const value = ref<Array<string>>(['ts', 'go']);
</script>
<template>
<CfSelect
v-model="value"
multiple
clearable
:options="options"
placeholder="选一种或多种语言"
style="max-width: 360px;"
/>
<p style="margin-top: 8px; color: var(--fg-3); font-size: 12px;">
当前:{{ value.length ? value.join(' / ') : '空' }} · 用退格键删除最近的标签
</p>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'go', label: 'Go' },
{ value: 'rs', label: 'Rust' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
];
const value = ref<Array<string>>(['ts', 'go']);
</script>
<template>
<CfSelect
v-model="value"
multiple
clearable
:options="options"
placeholder="选一种或多种语言"
style="max-width: 360px;"
/>
<p style="margin-top: 8px; color: var(--fg-3); font-size: 12px;">
当前:{{ value.length ? value.join(' / ') : '空' }} · 用退格键删除最近的标签
</p>
</template> Searchable
searchable adds an input atop the menu that filters by label in real time. Stacks with multiple for traditional multi-select UX.
<script setup lang="ts">
import { computed, ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const COUNTRIES = [
'中国', '日本', '韩国', '美国', '加拿大', '英国', '法国', '德国',
'意大利', '西班牙', '葡萄牙', '荷兰', '比利时', '瑞士', '瑞典',
'挪威', '丹麦', '芬兰', '俄罗斯', '巴西', '墨西哥', '阿根廷',
'澳大利亚', '新西兰', '南非', '埃及', '印度', '新加坡', '马来西亚',
'泰国', '越南', '印度尼西亚', '土耳其', '希腊', '波兰', '捷克',
];
const options: SelectOption[] = COUNTRIES.map((c) => ({ value: c, label: c }));
const value = ref<string | null>(null);
</script>
<template>
<CfSelect
v-model="value"
searchable
clearable
:options="options"
placeholder="选择国家 / 地区"
style="max-width: 280px;"
/>
</template> <script setup>
import { computed, ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const COUNTRIES = [
'中国', '日本', '韩国', '美国', '加拿大', '英国', '法国', '德国',
'意大利', '西班牙', '葡萄牙', '荷兰', '比利时', '瑞士', '瑞典',
'挪威', '丹麦', '芬兰', '俄罗斯', '巴西', '墨西哥', '阿根廷',
'澳大利亚', '新西兰', '南非', '埃及', '印度', '新加坡', '马来西亚',
'泰国', '越南', '印度尼西亚', '土耳其', '希腊', '波兰', '捷克',
];
const options= COUNTRIES.map((c) => ({ value: c, label: c }));
const value = ref<string | null>(null);
</script>
<template>
<CfSelect
v-model="value"
searchable
clearable
:options="options"
placeholder="选择国家 / 地区"
style="max-width: 280px;"
/>
</template> Groups
Add group: string to an option and the component renders a section header per group (in first-encounter order).
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'us-east', label: 'US East', group: '美洲' },
{ value: 'us-west', label: 'US West', group: '美洲' },
{ value: 'sa-east', label: 'SA East', group: '美洲' },
{ value: 'eu-west', label: 'EU West', group: '欧洲' },
{ value: 'eu-central', label: 'EU Central', group: '欧洲' },
{ value: 'ap-tokyo', label: 'AP Tokyo', group: '亚太' },
{ value: 'ap-singapore', label: 'AP Singapore', group: '亚太' },
{ value: 'ap-shanghai', label: 'AP Shanghai', group: '亚太' },
];
const value = ref<string | null>('eu-west');
</script>
<template>
<CfSelect
v-model="value"
searchable
:options="options"
placeholder="选择 region"
style="max-width: 280px;"
/>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'us-east', label: 'US East', group: '美洲' },
{ value: 'us-west', label: 'US West', group: '美洲' },
{ value: 'sa-east', label: 'SA East', group: '美洲' },
{ value: 'eu-west', label: 'EU West', group: '欧洲' },
{ value: 'eu-central', label: 'EU Central', group: '欧洲' },
{ value: 'ap-tokyo', label: 'AP Tokyo', group: '亚太' },
{ value: 'ap-singapore', label: 'AP Singapore', group: '亚太' },
{ value: 'ap-shanghai', label: 'AP Shanghai', group: '亚太' },
];
const value = ref<string | null>('eu-west');
</script>
<template>
<CfSelect
v-model="value"
searchable
:options="options"
placeholder="选择 region"
style="max-width: 280px;"
/>
</template> Async loading
When loading=true the trigger is disabled and shows a spinner; the menu shows “Loading…”. Pair with onMounted / SWR / TanStack Query.
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const loading = ref(true);
const options = ref<SelectOption[]>([]);
const value = ref<string | null>(null);
onMounted(() => {
setTimeout(() => {
options.value = [
{ value: 'a', label: '从远端加载的选项 A' },
{ value: 'b', label: '从远端加载的选项 B' },
{ value: 'c', label: '从远端加载的选项 C' },
];
loading.value = false;
}, 1200);
});
</script>
<template>
<CfSelect
v-model="value"
:options="options"
:loading="loading"
placeholder="loading=true 时显示 spinner"
style="max-width: 280px;"
/>
</template> <script setup>
import { onMounted, ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const loading = ref(true);
const options = ref<SelectOption[]>([]);
const value = ref<string | null>(null);
onMounted(() => {
setTimeout(() => {
options.value = [
{ value: 'a', label: '从远端加载的选项 A' },
{ value: 'b', label: '从远端加载的选项 B' },
{ value: 'c', label: '从远端加载的选项 C' },
];
loading.value = false;
}, 1200);
});
</script>
<template>
<CfSelect
v-model="value"
:options="options"
:loading="loading"
placeholder="loading=true 时显示 spinner"
style="max-width: 280px;"
/>
</template> Keyboard interaction
| Key | Behavior |
|---|---|
↓ / ↑ | Open the menu / move active among options |
Enter / Space | Open the menu / select the active option |
Esc | Close the menu |
Tab | Close the menu and return focus to the browser |
API
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectOption[] | [] | Option array, { value, label, disabled? } |
placeholder | string | 'Select' | Shown when no value is selected |
variant | 'outline' | 'filled' | 'ghost' | 'outline' | Visual variant |
size | 'sm' | 'md' | 'lg' | 'md' | Overall size |
clearable | boolean | false | Show clear button when a value is selected |
disabled | boolean | false | Disable the whole control |
error | boolean | false | Error state |
name | string | — | Renders a hidden input for native form submit |
id | string | — | Trigger button id, also wires up the listbox |
multiple | boolean | false | Multi-select; model becomes Array<value> |
searchable | boolean | false | Adds a filter input above the menu |
loading | boolean | false | Async loading state (trigger disabled + spinner) |
emptyText | string | 'No options' | Override the empty-state message |
maxTagCount | number | — | Cap visible tags in multi-mode; rest collapse |
Events
| Vue event | React callback | payload | Description |
|---|---|---|---|
change | onChange | (value, { option }) | Value changed; on clear, option is null |
select | onSelect | option | An option was selected |
clear | onClear | — | Clear button clicked |
open-change | onOpenChange | open | Menu opened / closed |
active-change | onActiveChange | (option, index) | Keyboard or mouse moved the active option |
search | onSearch | term | Search input changed (only when searchable=true) |
focus / blur | onFocus / onBlur | FocusEvent | Trigger button focus events |
The current implementation uses an
position: absoluteoverlay. If a parent hasoverflow: hiddenand crops the menu, a Portal version will be added later.
反馈与讨论
Select · Discussion