GrRadio

Пакет: @feugene/granularityядроГруппа: Формы

Берут, когда переключатель стоит отдельно.

Когда брать

  • переключатель стоит отдельно — вариант в строке таблицы, в карточке, в ячейке списка: группы вокруг него нет;
  • раскладку задаёт потребитель — варианты разложены по своей сетке, а не в ряд или столбец;
  • вид кнопки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 и modelValuestring | 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

PropTypeпо умолчаниюОписание
variantGrRadioVariant | undefined"radiobox"
modelValueGrRadioValue | undefinedundefined
disabledboolean | undefinedundefined
invalidboolean | undefinedfalseВизуальное и ARIA-состояние ошибки. Складывается с `invalid` группы.
requiredboolean | undefinedfalse
size"xs" | "sm" | "md" | "lg" | undefinedundefined
ariaLabelstring | undefinedundefined
namestring | undefinedundefined
formstring | undefinedundefined
idstring | undefinedundefined
buttonVariantGrButtonVariant | undefined"outline"
buttonTone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"neutral"
selectedButtonVariantGrButtonVariant | undefined"primary"
selectedButtonTone"primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined"primary"
valueобязательныйGrRadioValue

Slots

SlotTypeОписание
defaultanyПодпись переключателя вместо пропа `label`.
descriptionanyПояснение под подписью.

Events

EventTypeОписание
update:modelValue[value: GrRadioValue]

Примеры 3

Описания, ошибка и числовые значения

Слот #description связан с переключателем через aria-describedby, invalid приходит от группы, а value — число.

Выбран тариф #2. Группа — одна остановка `Tab`: внутри работают стрелки, `Home` и `End`, отключённый вариант пропускается.

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>

Отдельные переключатели с общей моделью

Минимальный контролируемый сценарий без group-wrapper, полезный там, где нужно вручную разложить отдельные radio по кастомному layout.

Current delivery cadence:
Weekly digest

Standalone
<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.

Button-like radios keep the same `v-model` contract while matching toolbar and segmented-control layouts.

Button Variant
<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, отключённые варианты пропускаются

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты