GrRadio
Берут, когда переключатель стоит отдельно.
Когда брать
- переключатель стоит отдельно — вариант в строке таблицы, в карточке, в ячейке списка: группы вокруг него нет;
- раскладку задаёт потребитель — варианты разложены по своей сетке, а не в ряд или столбец;
- вид кнопки —
variant="button"даёт внешностьGrButton, оставляя рольradio; - подпись сложная — слот принимает разметку: цену, бейдж, иконку.
Когда взять другое
| Нужно | Берите |
|---|---|
| Вариантов несколько и они рядом | GrRadioGroup |
| Можно выбрать несколько | GrCheckbox |
| Вариантов 2–5 и они переключают вид | GrSegmented |
| Вариантов больше семи | GrSelect |
Вне GrRadioGroup клавиатурный контракт паттерна radiogroup не работает:
стрелки между вариантами водит контейнер. Одиночный GrRadio — это осознанный
отказ от него ради своей раскладки, а не упрощение.
Клавиатура: группа — одна остановка `Tab`
| Клавиша | Что делает |
|---|---|
Tab | входит в группу и выходит из неё; внутри группы остановка ровно одна (roving tabindex) |
↓ / → | следующий вариант, по кругу |
↑ / ← | предыдущий вариант, по кругу |
Home / End | первый / последний доступный вариант |
Space, Enter | выбрать текущий (для автономного переключателя) |
Выбор едет вместе с фокусом — так требует паттерн radiogroup. Отключённые
варианты пропускаются: они не участвуют ни в roving tabindex, ни в обходе
стрелками.
Состояние живёт в GrRadioGroup (register / rovingValue / moveSelection /
selectEdge в контексте), а GrRadio спрашивает у группы, он ли сейчас
фокусируемый. Автономный переключатель вне группы остаётся обычной остановкой
Tab.
Подпись и описание
<GrRadio value="pro">
Pro
<template #description>
Списывается ежемесячно, можно отменить в любой момент
</template>
</GrRadio>
Подпись выбранного варианта — --gr-fg, невыбранного — --gr-muted-fg: до
этого приглушённой была любая, и выбранный вариант не выделялся текстом вовсе.
Слот #description связывается с переключателем через aria-describedby —
иначе для скринридера описания не существует.
Значения
value и modelValue — string | number | boolean (GrRadioValue).
Перечисления в реальных формах это обычно id числом или флаг. Объекты сюда не
входят намеренно: значение уходит в data-value и в скрытый input нативной
формы, а значит должно иметь однозначное строковое представление.
Ошибка и отключённое состояние
invalid можно поставить самому переключателю или всей группе — состояния
складываются по «или». Рамка контрола красится --gr-danger, на элемент идёт
aria-invalid.
Отключённый переключатель гасится токенами (--gr-muted / --gr-muted-fg), а
не прозрачностью: opacity разбавляет выверенные на AA цвета. Кнопочный вариант
берёт отключённый вид у самого GrButton — там он вариант-зависимый.
Нативная форма
Рядом с переключателем рендерится input[type="hidden"] — только у выбранного
и не-disabled элемента и только при заданном name, как поступил бы нативный
radio. Внутрь элемента с role="radio" ничего интерактивного не вкладывается:
роль объявляет потомков презентационными, и скрытый <input type="radio">
сломал бы виджет для скринридера (axe: nested-interactive).
Playground 10
Загружается…
<GrRadio />Установка
npm i @feugene/granularityИмпорт
import { GrRadio } from '@feugene/granularity/components/GrRadio'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
variant | GrRadioVariant | undefined | "radiobox" | — |
modelValue | GrRadioValue | undefined | undefined | — |
disabled | boolean | undefined | undefined | — |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. Складывается с `invalid` группы. |
required | boolean | undefined | false | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | — |
form | string | undefined | undefined | — |
id | string | undefined | undefined | — |
buttonVariant | GrButtonVariant | undefined | "outline" | — |
buttonTone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | "neutral" | — |
selectedButtonVariant | GrButtonVariant | undefined | "primary" | — |
selectedButtonTone | "primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined | "primary" | — |
valueобязательный | GrRadioValue | — | — |
Slots
| Slot | Type | Описание |
|---|---|---|
default | any | Подпись переключателя вместо пропа `label`. |
description | any | Пояснение под подписью. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: GrRadioValue] | — |
Примеры 3
Описания, ошибка и числовые значения
Слот #description связан с переключателем через aria-describedby, invalid приходит от группы, а value — число.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrFormField, GrRadio, GrRadioGroup } from '@feugene/granularity'
// Значения числовые: перечисления в реальных формах — это обычно id, а не строка.
const planId = ref(2)
const plans = [
{ id: 1, label: 'Команда', description: 'До 10 участников, общий проект' },
{ id: 2, label: 'Бизнес', description: 'Роли, аудит-лог, приоритетная поддержка' },
{ id: 3, label: 'Enterprise', description: 'Только по договору', disabled: true },
]
const confirmed = ref(false)
const error = computed(() => (confirmed.value && planId.value === 1 ? 'Для аудит-лога нужен тариф выше' : ''))
</script>
<template>
<div class="grid gap-4">
<GrFormField label="Тариф" :error="error">
<GrRadioGroup v-model="planId" name="plan" :invalid="Boolean(error)">
<GrRadio
v-for="plan in plans"
:key="plan.id"
:value="plan.id"
:disabled="plan.disabled"
>
{{ plan.label }}
<template #description>
{{ plan.description }}
</template>
</GrRadio>
</GrRadioGroup>
</GrFormField>
<label class="flex items-center gap-2 text-sm text-[var(--gr-muted-fg)]">
<input v-model="confirmed" type="checkbox">
Проверять требование аудит-лога
</label>
<div class="rounded-2xl border border-dashed border-[var(--gr-brd)] p-3 text-sm text-[var(--gr-muted-fg)]">
Выбран тариф <span class="font-semibold text-[var(--gr-fg)]">#{{ planId }}</span>.
Группа — одна остановка `Tab`: внутри работают стрелки, `Home` и `End`, отключённый вариант пропускается.
</div>
</div>
</template>Отдельные переключатели с общей моделью
Минимальный контролируемый сценарий без group-wrapper, полезный там, где нужно вручную разложить отдельные radio по кастомному layout.
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrRadio } from '@feugene/granularity'
const delivery = ref('weekly')
const selectedLabel = computed(() => {
const labels: Record<string, string> = {
daily: 'Daily digest',
weekly: 'Weekly digest',
monthly: 'Monthly report',
}
return labels[delivery.value] ?? delivery.value
})
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]">
<div class="grid gap-3">
<GrRadio v-model="delivery" name="digest-frequency" value="daily">
Daily digest
</GrRadio>
<GrRadio v-model="delivery" name="digest-frequency" value="weekly">
Weekly digest
</GrRadio>
<GrRadio v-model="delivery" name="digest-frequency" value="monthly">
Monthly report
</GrRadio>
</div>
<div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4 text-sm text-[var(--gr-muted-fg)]">
Current delivery cadence:
<div class="mt-2 text-base font-semibold text-[var(--gr-fg)]">
{{ selectedLabel }}
</div>
</div>
</div>
</template>Кнопочный тон для сегментов
Отдельный пример для tone="button": по API это всё тот же radio, но визуально он работает как сегментированный toolbar-control.
<script setup lang="ts">
import { ref } from 'vue'
import { GrRadio } from '@feugene/granularity'
const density = ref('balanced')
</script>
<template>
<div class="grid gap-4">
<div class="flex flex-wrap items-center gap-3">
<GrRadio v-model="density" value="compact" variant="button" size="sm">
Compact
</GrRadio>
<GrRadio v-model="density" value="balanced" variant="button" size="sm">
Balanced
</GrRadio>
<GrRadio v-model="density" value="comfortable" variant="button" size="sm">
Comfortable
</GrRadio>
</div>
<div class="rounded-2xl border border-dashed border-[var(--gr-brd)] bg-[var(--gr-muted)]/35 p-4 text-sm text-[var(--gr-muted-fg)]">
Button-like radios keep the same `v-model` contract while matching toolbar and segmented-control layouts.
</div>
</div>
</template>Доступность
- Паттерн APG
radio- Клавиши
Space,Enter— выбрать;↓/→— следующий,↑/←— предыдущий (выбор едет вместе с фокусом, по кругу);Home/End— первый/последний доступный. Группа — одна остановкаTab, отключённые варианты пропускаются