GrRadio
A single choice within a group of mutually exclusive options.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the switch stands on its own — an option in a table row, in a card, in a list cell: there is no group around it;
- the layout is set by the consumer — the options are laid out on a grid of their own rather than in a row or a column;
- the look of a button —
variant="button"gives the appearance ofGrButtonwhile keeping theradiorole; - the label is complex — the slot accepts markup: a price, a badge, an icon.
When to take something else
| Need | Take |
|---|---|
| There are several options and they stand side by side | GrRadioGroup |
| Several can be selected | GrCheckbox |
| There are 2–5 options and they switch a view | GrSegmented |
| There are more than seven options | GrSelect |
Outside GrRadioGroup the keyboard contract of the radiogroup pattern does not work: the
arrows between options are walked by the container. A single GrRadio is a deliberate refusal of
it for the sake of a layout of your own rather than a simplification.
The keyboard: the group is one `Tab` stop
| Key | What it does |
|---|---|
Tab | enters the group and leaves it; inside the group there is exactly one stop (a roving tabindex) |
↓ / → | the next option, in a ring |
↑ / ← | the previous option, in a ring |
Home / End | the first / the last available option |
Space, Enter | select the current one (for a standalone switch) |
The selection travels with the focus — that is what the radiogroup pattern requires. Disabled
options are skipped: they take part neither in the roving tabindex nor in the walk with the
arrows.
The state lives in GrRadioGroup (register / rovingValue / moveSelection / selectEdge in
the context), and GrRadio asks the group whether it is the focusable one right now. A standalone
switch outside a group remains an ordinary Tab stop.
The label and the description
<GrRadio value="pro">
Pro
<template #description>
Charged monthly, can be cancelled at any moment
</template>
</GrRadio>
The label of a selected option is --gr-fg, of an unselected one --gr-muted-fg: before that any
of them was dimmed, and the selected option was not distinguished by text at all.
The #description slot is linked to the switch through aria-describedby — otherwise the
description does not exist for a screen reader.
The values
value and modelValue are string | number | boolean (GrRadioValue). Enumerations in real
forms are usually an id as a number or a flag. Objects are deliberately not included: the value
travels into data-value and into the hidden input of a native form, which means it has to have
an unambiguous string representation.
The error and the disabled state
invalid can be set on the switch itself or on the whole group — the states add by “or”. The
border of the control is coloured --gr-danger, and aria-invalid goes onto the element.
A disabled switch is dimmed with tokens (--gr-muted / --gr-muted-fg) rather than with
transparency: opacity dilutes colours tuned to AA. The button variant takes its disabled look
from GrButton itself — there it depends on the variant.
The native form
An input[type="hidden"] is rendered beside the switch — only for the selected and non-disabled
element and only when name is set, as a native radio would do. Nothing interactive is nested
inside an element with role="radio": the role declares its descendants presentational, and a
hidden <input type="radio"> would break the widget for a screen reader (axe:
nested-interactive).
Playground 10
Loading…
<GrRadio />Install
npm i @feugene/granularityImport
import { GrRadio } from '@feugene/granularity/components/GrRadio'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
variant | GrRadioVariant | undefined | "radiobox" | — |
modelValue | GrRadioValue | undefined | undefined | — |
disabled | boolean | undefined | undefined | — |
invalid | boolean | undefined | false | The visual and ARIA state of an error. It adds up with the `invalid` of the group. |
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" | — |
valuerequired | GrRadioValue | — | — |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The label of the radio instead of the `label` prop. |
description | any | An explanation under the label. |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: GrRadioValue] | — |
Examples 3
Descriptions
<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>Standalone radios with shared model
<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>Button tone for segmented controls
<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>Accessibility
- APG pattern
radio