GrRadioGroup
Берут, когда вариантов до семи и все видны.
Когда брать
- вариантов до семи и все видны — способ доставки, тип оплаты, роль: выбор без лишнего клика по панели;
- у вариантов есть описание — строка пояснения под подписью, чего не даёт селект;
- группа — часть формы — общее
name,disabledиreadonlyраздаются через контекст; - вариант выглядит кнопкой —
variant="button"без потери семантикиradiogroup.
Когда взять другое
| Нужно | Берите |
|---|---|
| Вариантов больше семи | GrSelect |
| Вариантов 2–5 и они переключают вид, а не значение | GrSegmented |
| Можно выбрать несколько | GrCheckboxGroup |
| Переключатель один и он про «включено/выключено» | GrSwitch |
| Один переключатель вне группы | GrRadio |
Два режима
<!-- Пропом: короткий путь для плоского списка. -->
<GrRadioGroup v-model="status" :options="options" />
<!-- Слотом: когда вариантам нужна своя разметка. -->
<GrRadioGroup v-model="status">
<GrRadio value="draft">Черновик</GrRadio>
<GrRadio value="review">На проверке</GrRadio>
</GrRadioGroup>
Оба режима получают одну и ту же клавиатуру и один и тот же контекст: состав группы собирается регистрацией детей, а не обходом DOM, поэтому работает и на сервере.
Опции
interface GrRadioGroupOption {
value: string | number | boolean
label: string
disabled?: boolean
description?: string
}
disabled отключает один вариант, не переводя всю группу на слот — раньше это
был единственный способ. description рисуется под подписью и только в
варианте radiobox: у кнопочного чипа описанию негде жить.
Раскладка
orientation — vertical (по умолчанию) или horizontal. Кнопочный вариант
всегда горизонтальный: его собирает GrButtonGroup, и orientation там ни на
что не влияет.
Состояния
disabled гасит всю группу, readonly оставляет выбор видимым, но
неизменяемым: aria-readonly объявляет сама группа (у роли radio такого
атрибута нет), а переключатели перестают обещать клик курсором.
invalid и required доходят и до группы, и до вида переключателей. Внутри
GrFormField то же самое приходит из контекста поля: имя через
aria-labelledby, подсказка и текст ошибки — через aria-describedby, ошибка —
через aria-invalid. Группа не labelable-элемент, поэтому <label for> для неё
неприменим.
Клавиатура
Вся раскладка описана в карточке GrRadio: группа — одна остановка Tab,
внутри ↑/↓/←/→ по кругу и Home/End на края. Отключённые варианты
пропускаются везде — и в roving tabindex, и при обходе стрелками.
Playground 7
Загружается…
<GrRadioGroup />Установка
npm i @feugene/granularityИмпорт
import { GrRadioGroup } from '@feugene/granularity/components/GrRadioGroup'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
variant | GrRadioGroupVariant | undefined | "radiobox" | — |
options | GrRadioGroupOption[] | undefined | undefined | — |
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Только для чтения: выбор видно, но он не меняется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | — |
orientation | GrRadioGroupOrientation | undefined | "vertical" | Раскладка варианта `radiobox`. Кнопочный вариант всегда горизонтальный — его собирает `GrButtonGroup`. |
modelValueобязательный | GrRadioValue | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Собственная разметка переключателей вместо генерации из `options`. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: GrRadioValue] | — |
change | [value: GrRadioValue] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Примеры 4
Группа, порождённая из списка опций
Быстрый старт-сценарий для options: одна декларация массива сразу даёт полную radio-группу без ручного рендера каждого элемента.
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { GrRadioGroupOrientation } from '@feugene/granularity'
import { GrRadioGroup, GrSegmented } from '@feugene/granularity'
const status = ref('review')
const orientation = ref<GrRadioGroupOrientation>('vertical')
const readonly = ref(false)
// Опция умеет быть отключённой и нести пояснение — без перехода на слот.
const options = [
{ value: 'draft', label: 'Draft', description: 'Виден только автору' },
{ value: 'review', label: 'In review', description: 'Ждёт решения редактора' },
{ value: 'published', label: 'Published', description: 'Опубликовано на сайте' },
{ value: 'archived', label: 'Archived', description: 'Доступно после снятия блокировки', disabled: true },
]
const selectedOption = computed(() => options.find(option => option.value === status.value)?.label ?? status.value)
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-4">
<GrSegmented
v-model="orientation"
size="sm"
:options="[
{ value: 'vertical', label: 'vertical' },
{ value: 'horizontal', label: 'horizontal' },
]"
/>
<label class="flex items-center gap-2 text-sm text-[var(--gr-muted-fg)]">
<input v-model="readonly" type="checkbox">
readonly
</label>
</div>
<GrRadioGroup
v-model="status"
:options="options"
:orientation="orientation"
:readonly="readonly"
/>
</div>
<div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-muted-fg)]">
Selected state:
<div class="mt-2 text-base font-semibold text-[var(--gr-fg)]">
{{ selectedOption }}
</div>
<div class="mt-3">
Отключённый вариант пропускается и стрелками, и `Tab`.
</div>
</div>
</div>
</template>Полная линейка размеров
Все четыре ступени рядом, в обоих вариантах. У кнопочного карта размеров общая с GrButton — кнопка той же ступени стоит рядом для сверки; у radiobox по ступеням масштабируются коробка, точка и подпись.
xssmmdlgxssmmdlg<script setup lang="ts">
import { ref } from 'vue'
import { GrButton, GrRadioGroup } from '@feugene/granularity'
const sizes = ['xs', 'sm', 'md', 'lg'] as const
const viewOptions = [
{ value: 'board', label: 'Board' },
{ value: 'calendar', label: 'Calendar' },
{ value: 'table', label: 'Table' },
]
const planOptions = [
{ value: 'free', label: 'Free' },
{ value: 'team', label: 'Team' },
]
// По значению на ступень: одна модель на все четыре сделала бы выбор общим, и
// разница между ступенями читалась бы хуже.
const buttonView = ref<Record<string, string>>({ xs: 'board', sm: 'calendar', md: 'board', lg: 'table' })
const radioboxPlan = ref<Record<string, string>>({ xs: 'free', sm: 'team', md: 'free', lg: 'team' })
</script>
<template>
<div class="grid gap-6">
<div class="grid gap-3">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
variant="button" — ступень в ступень с GrButton
</div>
<div
v-for="size in sizes"
:key="`button-${size}`"
class="flex flex-wrap items-center gap-3"
>
<code class="w-8 text-xs text-[var(--gr-muted-fg)]">{{ size }}</code>
<GrRadioGroup
v-model="buttonView[size]"
:options="viewOptions"
variant="button"
:size="size"
/>
<!-- Кнопка рядом той же ступени: у кнопочного варианта карта размеров общая
с `GrButton`, и высоты обязаны совпадать. -->
<GrButton :size="size" variant="outline">
GrButton {{ size }}
</GrButton>
</div>
</div>
<div class="grid gap-3">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
variant="radiobox" — коробка, точка и подпись тоже по ступеням
</div>
<div
v-for="size in sizes"
:key="`radiobox-${size}`"
class="flex flex-wrap items-center gap-3"
>
<code class="w-8 text-xs text-[var(--gr-muted-fg)]">{{ size }}</code>
<GrRadioGroup
v-model="radioboxPlan[size]"
:options="planOptions"
:size="size"
orientation="horizontal"
/>
</div>
</div>
</div>
</template>Свои слоты для пояснений к вариантам
Когда у опций есть secondary badges и статусы, удобнее перейти от options к slot-based composition поверх GrRadioGroup + GrRadio.
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrRadio, GrRadioGroup } from '@feugene/granularity'
const channel = ref('slack')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<GrRadioGroup v-model="channel" name="incident-channel">
<GrRadio value="slack">
<span class="inline-flex items-center gap-2">
Slack
<GrBadge tone="success" size="sm">Primary</GrBadge>
</span>
</GrRadio>
<GrRadio value="email">
<span class="inline-flex items-center gap-2">
Email
<GrBadge tone="warning" size="sm">Fallback</GrBadge>
</span>
</GrRadio>
<GrRadio value="pagerduty">
<span class="inline-flex items-center gap-2">
PagerDuty
<GrBadge tone="danger" size="sm">Escalation</GrBadge>
</span>
</GrRadio>
</GrRadioGroup>
<div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-muted-fg)]">
Routed through: <span class="font-semibold text-[var(--gr-fg)]">{{ channel }}</span>
</div>
</div>
</template>Inheritance
<script setup lang="ts">
import { ref } from 'vue'
import { GrRadio, GrRadioGroup, GrSwitch } from '@feugene/granularity'
const environment = ref('staging')
const disabled = ref(false)
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<GrRadioGroup v-model="environment" name="target-environment" :disabled="disabled">
<GrRadio value="local">Local preview</GrRadio>
<GrRadio value="staging">Staging</GrRadio>
<GrRadio value="production">Production</GrRadio>
</GrRadioGroup>
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrSwitch v-model="disabled" size="sm">
Disable full group
</GrSwitch>
<div class="text-sm text-[var(--gr-muted-fg)]">
Active target: <span class="font-semibold text-[var(--gr-fg)]">{{ environment }}</span>
</div>
</div>
</div>
</template>Доступность
- Паттерн APG
radio- Клавиши
Space,Enter— выбрать;↓/→— следующий,↑/←— предыдущий (выбор едет вместе с фокусом, по кругу);Home/End— первый/последний доступный. Группа — одна остановкаTab, отключённые варианты пропускаются