Selection Fields
Select, Combobox, RadioGroup, Checkbox, and more
Full example
The complete sandbox example (all select-family fields on one page), read directly from the
form-develop-app / form-develop-app-shadcn source at build time — switch skins to see the
same fields in both implementations.
'use client'
import { Box, Heading, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout, SubmittedDataPreview } from '../_components'
/**
* Демо-схема для NativeSelect и CascadingSelect
*/
const DemoSchema = z
.object({
// NativeSelect fields
simpleSelect: z.string().meta({
ui: { title: 'Simple NativeSelect', placeholder: 'Select option...' },
}),
sizeSelect: z.string().meta({
ui: { title: 'Size Selection' },
}),
prioritySelect: z
.string()
.optional()
.meta({
ui: { title: 'Priority (optional)' },
}),
// CascadingSelect fields
country: z.string().meta({
ui: { title: 'Country', placeholder: 'Select country...' },
}),
city: z
.string()
.optional()
.meta({
ui: { title: 'City', placeholder: 'Select city...' },
}),
// Вложенные поля
address: z.object({
region: z.string().meta({
ui: { title: 'Region', placeholder: 'Select region...' },
}),
district: z
.string()
.optional()
.meta({
ui: { title: 'District', placeholder: 'Select district...' },
}),
}),
// Group.Field.Select с getGroup (optgroup)
technology: z
.string()
.optional()
.meta({
ui: { title: 'Technology (grouped)', placeholder: 'Select technology...' },
}),
})
.strip()
type DemoData = z.infer<typeof DemoSchema>
const initialData: DemoData = {
simpleSelect: '',
sizeSelect: 'md',
prioritySelect: undefined,
country: '',
city: undefined,
address: {
region: '',
district: undefined,
},
technology: undefined,
}
// Данные для NativeSelect
const simpleOptions = [
{ title: 'Option 1', value: 'opt1' },
{ title: 'Option 2', value: 'opt2' },
{ title: 'Option 3', value: 'opt3' },
]
const sizeOptions = [
{ title: 'Extra Small', value: 'xs' },
{ title: 'Small', value: 'sm' },
{ title: 'Medium', value: 'md' },
{ title: 'Large', value: 'lg' },
]
const priorityOptions = [
{ title: 'Low Priority', value: 'low' },
{ title: 'Medium Priority', value: 'medium' },
{ title: 'High Priority', value: 'high' },
{ title: 'Critical', value: 'critical' },
]
// Данные для CascadingSelect
const countries = [
{ label: 'Russia', value: 'ru' },
{ label: 'USA', value: 'us' },
{ label: 'Germany', value: 'de' },
]
const citiesByCountry: Record<string, { label: string; value: string }[]> = {
ru: [
{ label: 'Moscow', value: 'msk' },
{ label: 'Saint Petersburg', value: 'spb' },
{ label: 'Novosibirsk', value: 'nsk' },
],
us: [
{ label: 'New York', value: 'nyc' },
{ label: 'Los Angeles', value: 'la' },
{ label: 'Chicago', value: 'chi' },
],
de: [
{ label: 'Berlin', value: 'ber' },
{ label: 'Munich', value: 'mun' },
{ label: 'Hamburg', value: 'ham' },
],
}
const regions = [
{ label: 'Moscow Region', value: 'msk_reg' },
{ label: 'Leningrad Region', value: 'len_reg' },
{ label: 'Krasnodar Region', value: 'krd_reg' },
]
// Данные для группированного Select (getGroup)
const technologies = [
{ label: 'React', value: 'react', category: 'Frontend' },
{ label: 'Vue', value: 'vue', category: 'Frontend' },
{ label: 'Svelte', value: 'svelte', category: 'Frontend' },
{ label: 'Express', value: 'express', category: 'Backend' },
{ label: 'NestJS', value: 'nestjs', category: 'Backend' },
{ label: 'React Native', value: 'react-native', category: 'Mobile' },
]
const districtsByRegion: Record<string, { label: string; value: string }[]> = {
msk_reg: [
{ label: 'Odintsovo', value: 'odin' },
{ label: 'Khimki', value: 'khim' },
{ label: 'Balashikha', value: 'bal' },
],
len_reg: [
{ label: 'Vsevolozhsk', value: 'vsev' },
{ label: 'Gatchina', value: 'gat' },
{ label: 'Vyborg', value: 'vyb' },
],
krd_reg: [
{ label: 'Sochi', value: 'soc' },
{ label: 'Novorossiysk', value: 'nov' },
{ label: 'Anapa', value: 'ana' },
],
}
export default function SelectDemoPage() {
const [submitted, setSubmitted] = useState<DemoData | null>(null)
return (
<DemoPageLayout title="Select Demo" description="NativeSelect и CascadingSelect компоненты" maxW="800px">
<Form
schema={DemoSchema}
initialValue={initialData}
onSubmit={(data) => {
setSubmitted(data)
}}
>
{/* NativeSelect секция */}
<Box borderWidth={1} borderRadius="md" p={4} mb={6}>
<Heading size="md" mb={4}>
NativeSelect
</Heading>
<Text color="fg.muted" mb={4}>
Нативный браузерный select для лучшего UX на мобильных устройствах
</Text>
<VStack gap={4} align="stretch">
<Form.Field.NativeSelect name="simpleSelect" options={simpleOptions} />
<Form.Field.NativeSelect name="sizeSelect" options={sizeOptions} />
<Form.Field.NativeSelect name="prioritySelect" options={priorityOptions} />
</VStack>
</Box>
{/* CascadingSelect секция */}
<Box borderWidth={1} borderRadius="md" p={4} mb={6}>
<Heading size="md" mb={4}>
CascadingSelect
</Heading>
<Text color="fg.muted" mb={4}>
Каскадный select с зависимостью от другого поля (Страна → Город)
</Text>
<VStack gap={4} align="stretch">
<Form.Field.Select name="country" options={countries} />
<Form.Field.CascadingSelect
name="city"
dependsOn="country"
loadOptions={async (parentValue) => {
// Имитация загрузки с сервера
await new Promise((r) => setTimeout(r, 300))
const countryCode = parentValue as string | undefined
if (!countryCode) {
return []
}
return citiesByCountry[countryCode] ?? []
}}
/>
</VStack>
</Box>
{/* Вложенные CascadingSelect */}
<Box borderWidth={1} borderRadius="md" p={4} mb={6}>
<Heading size="md" mb={4}>
Nested CascadingSelect
</Heading>
<Text color="fg.muted" mb={4}>
Каскадные select с вложенными путями (address.region → address.district)
</Text>
<VStack gap={4} align="stretch">
<Form.Field.Select name="address.region" options={regions} />
<Form.Field.CascadingSelect
name="address.district"
dependsOn="address.region"
loadOptions={async (parentValue) => {
await new Promise((r) => setTimeout(r, 200))
const regionCode = parentValue as string | undefined
if (!regionCode) {
return []
}
return districtsByRegion[regionCode] ?? []
}}
/>
</VStack>
</Box>
{/* Группированный Select (getGroup) */}
<Box borderWidth={1} borderRadius="md" p={4} mb={6}>
<Heading size="md" mb={4}>
Grouped Select (getGroup)
</Heading>
<Text color="fg.muted" mb={4}>
Опции сгруппированы по категории (optgroup) — симметрично группировке в Form.Field.Combobox
</Text>
<Form.Field.Select
name="technology"
options={technologies}
getGroup={(opt) => (opt as (typeof technologies)[number]).category}
/>
</Box>
<Form.Button.Submit>Submit</Form.Button.Submit>
</Form>
<SubmittedDataPreview data={submitted} />
</DemoPageLayout>
)
}'use client'
import { FieldCascadingSelect, FieldCombobox, FieldNativeSelect, FieldSelect } from '@letar/forms-shadcn'
import { useState } from 'react'
import { DemoForm, DemoPageLayout, SubmittedDataPreview } from '../_components'
const frameworkOptions = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
]
const shippingCountryOptions = [
{ label: 'Россия', value: 'ru' },
{ label: 'Казахстан', value: 'kz' },
]
const CITIES_BY_COUNTRY: Record<string, { label: string; value: string }[]> = {
ru: [{ label: 'Москва', value: 'msk' }, { label: 'Казань', value: 'kzn' }],
kz: [{ label: 'Алматы', value: 'alm' }, { label: 'Астана', value: 'ast' }],
}
interface SelectValues {
framework: string
country: string
frameworkSearch: string
shippingCountry: string
shippingCity: string
}
const defaultValues: SelectValues = {
framework: '',
country: '',
frameworkSearch: '',
shippingCountry: '',
shippingCity: '',
}
export default function SelectDemoPage() {
const [submitted, setSubmitted] = useState<SelectValues | null>(null)
return (
<DemoPageLayout
title="Select-поля"
description="Select, NativeSelect, Combobox, CascadingSelect (зависимый select)"
>
<DemoForm<SelectValues> defaultValues={defaultValues} onSubmit={setSubmitted}>
<FieldSelect name="framework" label="Фреймворк" options={frameworkOptions} placeholder="Выберите" />
<FieldNativeSelect
name="country"
label="Страна"
options={[
{ label: 'Россия', value: 'ru' },
{ label: 'Казахстан', value: 'kz' },
]}
/>
<FieldCombobox name="frameworkSearch" label="Поиск фреймворка" options={frameworkOptions} />
<FieldSelect
name="shippingCountry"
label="Страна доставки"
options={shippingCountryOptions}
placeholder="Выберите"
/>
<FieldCascadingSelect
name="shippingCity"
label="Город доставки"
dependsOn="shippingCountry"
loadOptions={async (country) => CITIES_BY_COUNTRY[country ?? ''] ?? []}
placeholderWhenDisabled="Сначала выберите страну"
/>
<button
type="submit"
className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm font-medium"
>
Отправить
</button>
</DemoForm>
<SubmittedDataPreview data={submitted} />
</DemoPageLayout>
)
}import { AppForm } from '@letar/forms-vue'
import { defineComponent, h, ref } from 'vue'
import { z } from 'zod'
import { FieldSelect } from '../../src/index'
/**
* Изолированный пример FieldSelect — самодостаточный файл (своя Zod-схема, свой AppForm).
*/
const schema = z.object({
category: z.string().meta({ ui: { title: 'Категория' } }),
})
const CATEGORY_OPTIONS = [
{ value: 'furniture', label: 'Мебель' },
{ value: 'electronics', label: 'Электроника' },
{ value: 'books', label: 'Книги' },
]
export const SelectDemo = defineComponent({
name: 'SelectDemo',
setup() {
const submitted = ref<Record<string, unknown> | null>(null)
return () =>
h('div', { class: 'space-y-6' }, [
h(
AppForm,
{
schema,
initialValue: { category: '' },
onSubmit: (value: Record<string, unknown>) => {
submitted.value = value
},
},
{
default: () => [
h(FieldSelect, { name: 'category', options: CATEGORY_OPTIONS }),
h(
'button',
{ type: 'submit', class: 'bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm' },
'Сохранить',
),
],
},
),
submitted.value
? h('pre', { class: 'bg-muted mt-4 rounded-md p-3 text-xs' }, JSON.stringify(submitted.value, null, 2))
: null,
])
},
})Angular-пример для этого поля появится позже.
Select
API: Form.Field.Select
Dropdown select for choosing from a list of options.
const Schema = z.object({
category: z.enum(['electronics', 'clothing', 'books']).meta({
ui: { title: 'Category' },
}),
})
<Form.Field.Select name="category" />Options are automatically generated from z.enum(). For custom labels:
const Schema = z.object({
status: z.enum(['active', 'inactive', 'pending']).meta({
ui: {
title: 'Status',
options: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'pending', label: 'Pending Review' },
],
},
}),
})Combobox
API: Form.Field.Combobox
Searchable select with autocomplete. Great for large option lists.
const Schema = z.object({
country: z.string().meta({
ui: { title: 'Country', placeholder: 'Search countries...' },
}),
})
<Form.Field.Combobox
name="country"
options={countries.map(c => ({ value: c.code, label: c.name }))}
/>Async Search
Load options from a query hook as the user types via useQuery — signature matches
TanStack Query ({ data, isLoading, error }):
<Form.Field.Combobox
name="user"
useQuery={(search) =>
useFindManyUser({
where: { name: { contains: search, mode: 'insensitive' } },
take: 20,
})
}
getLabel={(u) => u.name}
getValue={(u) => u.id}
/>Editing an existing value with async search
With static options, the label for the current value is always available, so the field shows
it as soon as it mounts. With useQuery, the item matching the current value may not be in the
first (pre-search) result page — there's nothing to look the label up in yet. Pass initialLabel
explicitly when editing an entity with a pre-selected value, otherwise the field shows an empty
input even though the value is set:
<Form.Field.Combobox
name="userId"
useQuery={(search) =>
useFindManyUser({
where: { name: { contains: search, mode: 'insensitive' } },
take: 20,
})
}
getLabel={(u) => u.name}
getValue={(u) => u.id}
initialLabel={initialValues.userName}
/>RadioGroup
API: Form.Field.RadioGroup
Radio button group for single selection.
const Schema = z.object({
plan: z.enum(['free', 'pro', 'enterprise']).meta({
ui: { title: 'Plan' },
}),
})
<Form.Field.RadioGroup name="plan" />Checkbox
API: Form.Field.Checkbox
Standard checkbox for boolean values.
const Schema = z.object({
agree: z.boolean().meta({
ui: { title: 'I agree to the terms and conditions' },
}),
})
<Form.Field.Checkbox name="agree" />Switch
API: Form.Field.Switch
Toggle switch — visually distinct from checkbox.
const Schema = z.object({
notifications: z.boolean().meta({
ui: { title: 'Enable notifications' },
}),
})
<Form.Field.Switch name="notifications" />Tags
API: Form.Field.Tags
Tag input with autocomplete. Users can add multiple tags.
const Schema = z.object({
skills: z.array(z.string()).meta({
ui: { title: 'Skills', placeholder: 'Add a skill...' },
}),
})
<Form.Field.Tags name="skills" />
// With max limit
<Form.Field.Tags name="skills" maxTags={5} />RadioCard
API: Form.Field.RadioCard
Card-based selection — visually richer than RadioGroup. Supports description per option.
const options = [
{ value: 'starter', label: 'Starter', description: '$0/mo — for side projects' },
{ value: 'pro', label: 'Pro', description: '$29/mo — for teams' },
{ value: 'enterprise', label: 'Enterprise', description: '$99/mo — unlimited' },
]
<Form.Field.RadioCard name="plan" options={options} />CheckboxCard
API: Form.Field.CheckboxCard
Card-based multiple selection. Like RadioCard but allows selecting multiple items.
<Form.Field.CheckboxCard
name="features"
options={[
{ value: 'auth', label: 'Authentication', description: 'OAuth, magic links' },
{ value: 'payments', label: 'Payments', description: 'Stripe integration' },
{ value: 'analytics', label: 'Analytics', description: 'Usage tracking' },
]}
/>Listbox
API: Form.Field.Listbox
Dropdown list for single or multiple selection. Alternative to Select for larger lists.
// Single selection
<Form.Field.Listbox name="timezone" options={timezoneOptions} />
// Multiple selection
<Form.Field.Listbox name="permissions" options={permissionOptions} selectionMode="multiple" />NativeSelect
API: Form.Field.NativeSelect
Standard HTML <select> element. Lighter than Select, uses native browser UI.
<Form.Field.NativeSelect name="country" options={countryOptions} />SegmentedGroup
API: Form.Field.SegmentedGroup
Segmented control — pill-shaped toggle group for small option sets.
<Form.Field.SegmentedGroup
name="view"
options={[
{ value: 'grid', label: 'Grid' },
{ value: 'list', label: 'List' },
{ value: 'table', label: 'Table' },
]}
/>