GrCheckbox

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

Берут, когда согласие или флаг в форме.

Когда брать

  • согласие или флаг в форме — «запомнить меня», «согласен с условиями»: значение уедет с отправкой;
  • выбор строк списка — чекбокс в заголовке с indeterminate показывает частичный выбор;
  • подпись интерактивна — она живёт снаружи роли, поэтому ссылка внутри неё остаётся ссылкой;
  • нужна нативная форма — скрытый <input type="checkbox"> уходит с name и value.

Когда взять другое

НужноБерите
Настройка применяется немедленноGrSwitch
Вариантов несколько и они связаныGrCheckboxGroup
Можно выбрать только один вариантGrRadio
Переключаются режимы отображенияGrSegmented

Граница со GrSwitch проходит по моменту применения: чекбокс — значение формы, оно уезжает с сохранением; переключатель меняет состояние системы сразу, и кнопки «Сохранить» рядом с ним быть не должно.

`required` объявляется, но не проверяется браузером

required доезжает до aria-required на span[role="checkbox"] и не выставляется на скрытом инпуте. Нативная проверка потребовала бы от браузера сфокусировать невалидный контрол, а он невидим и aria-hidden: Chrome в таком случае отменяет отправку всей формы и пишет в консоль «An invalid form control … is not focusable» — то есть required не мешал бы отправить, а ломал бы отправку молча.

Проверяет обязательность правило формы:

<script setup lang="ts">
// `required` пустым считает `null`/`''`/`[]`, но не `false`: снятый чекбокс —
// это законное значение поля. «Согласие обязательно» — это `validator`.
const rules: GrFormRules = {
  terms: [{ validator: value => value === true || 'Примите условия' }],
}
</script>

<template>
  <GrForm :model="model" :rules="rules">
    <GrFormField name="terms" label="Условия">
      <GrCheckbox v-model="model.terms" required />
    </GrFormField>
  </GrForm>
</template>

Подпись живёт снаружи роли-виджета

role="checkbox" объявляет своих потомков презентационными, поэтому внутри него нет ни нативного инпута, ни содержимого слота: имя виджета приходит через aria-labelledby. Ради этого подпись и вынесена наружу — она может содержать ссылку на политику или кнопку «показать изменения».

Следствие: клик по подписи чекбокс переключает, а клик по её интерактивному содержимому (a, button, input, select, textarea, вложенный label, [role="button"], [role="link"]) — нет, такой элемент адресует сам себя.

labelPosition="start" ставит подпись перед контролом.

Состояния

disabled и invalid показываются токенами фона и рамки, а не прозрачностью: opacity разбавляет выверенные на AA токены текста. readonly оставляет значение в форме и возвращает нативный инпут к модели, если его переключил клик по внешнему <label for>.

Внутри GrFormField контрол получает id на виджет, aria-describedby, aria-invalid и aria-required из контекста поля.

Playground 13

Загружается…

Код
<GrCheckbox />

Установка

npm i @feugene/granularity

Импорт

import { GrCheckbox } from '@feugene/granularity/components/GrCheckbox'

API

Props

PropTypeпо умолчаниюОписание
modelValueboolean | undefinedundefinedНе задан внутри `GrCheckboxGroup` — состояние берётся из группы.
disabledboolean | undefinedundefined
readonlyboolean | undefinedfalseТолько для чтения: состояние видно, но не переключается.
invalidboolean | undefinedfalseВизуальное и ARIA-состояние ошибки.
requiredboolean | undefinedfalseОбязательное поле: объявляется как `aria-required`, нативной проверки нет.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedРазмер контрола. Не задан — берётся из группы, затем из `GrConfigProvider`, иначе `md`.
ariaLabelstring | undefinedundefinedИмя контрола, когда подписи в слоте нет (или она чисто визуальная).
namestring | undefinedundefined
valuestring | undefined"on"
formstring | undefinedundefined
idstring | undefinedundefinedПробрасывается на скрытый нативный `<input>`, чтобы работал `<label for="...">`.
indeterminateboolean | undefinedfalseПромежуточное («смешанное») состояние: `aria-checked="mixed"`, индикатор — тире.
labelPosition"end" | "start" | undefined"end"Сторона подписи относительно контрола.

Slots

SlotTypeОписание
defaultanyПодпись флажка. Может содержать ссылки: роль остаётся на самом контроле.

Events

EventTypeОписание
update:modelValue[value: boolean]
change[value: boolean]
focus[event: FocusEvent]
blur[event: FocusEvent]

Methods / Expose

Methods / ExposeTypeОписание
focus() => void
blur() => void

Примеры 4

Размеры вровень с остальной строкой формы

Чекбокс живёт на той же размерной шкале, что GrInput, GrRadio и GrButton (xslg), поэтому смешанная форма выравнивается сама — и глобально, через GrConfigProvider.

xs
sm
md
lg
Compact rows
Partial digest selection

Sizes
<script setup lang="ts">
import { ref } from 'vue'

import { GrCheckbox, GrFormField, GrInput } from '@feugene/granularity'

const sizes = ['xs', 'sm', 'md', 'lg'] as const

const size = ref<typeof sizes[number]>('md')
const compact = ref(true)
const digest = ref(false)
const project = ref('Granularity')
</script>

<template>
  <div class="grid gap-4">
    <div class="flex flex-wrap items-center gap-4">
      <GrCheckbox
        v-for="value in sizes"
        :key="value"
        :model-value="size === value"
        :size="value"
        @update:model-value="size = value"
      >
        {{ value }}
      </GrCheckbox>
    </div>

    <div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <!-- Подпись через GrFormField: он выдаёт контролу id и связывает с ним
           `<label for>`. Нарисованный рядом текст доступным именем не становится. -->
      <GrFormField label="Project name">
        <GrInput v-model="project" :size="size" />
      </GrFormField>

      <GrCheckbox v-model="compact" :size="size">
        Compact rows
      </GrCheckbox>
      <GrCheckbox v-model="digest" :size="size" indeterminate>
        Partial digest selection
      </GrCheckbox>
    </div>
  </div>
</template>

Отмечен, снят и заперт

Показываем базовую матрицу состояний: управляемые чекбоксы, отдельный disabled-case и компактную сводку по текущему выбору.

Weekly product digest
Incident alerts
Security bulletins are always enabled
Accept the notification policy
Label before the control
Selection summary
1 of 2 optional channels are active.

States
<script setup lang="ts">
import { computed, ref } from 'vue'

import { GrCheckbox, GrSwitch } from '@feugene/granularity'

const weeklyDigest = ref(true)
const incidentAlerts = ref(false)
const controlsDisabled = ref(false)
const terms = ref(false)
const compactRow = ref(true)

const enabledCount = computed(() => [weeklyDigest.value, incidentAlerts.value].filter(Boolean).length)
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]">
    <div class="grid gap-3">
      <GrCheckbox v-model="weeklyDigest" :disabled="controlsDisabled">
        Weekly product digest
      </GrCheckbox>
      <GrCheckbox v-model="incidentAlerts" :disabled="controlsDisabled">
        Incident alerts
      </GrCheckbox>
      <GrCheckbox :model-value="true" disabled>
        Security bulletins are always enabled
      </GrCheckbox>
      <GrCheckbox v-model="terms" :invalid="!terms" required>
        Accept the notification policy
      </GrCheckbox>
      <GrCheckbox v-model="compactRow" label-position="start">
        Label before the control
      </GrCheckbox>
    </div>

    <div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
      <div>
        <div class="text-sm font-semibold text-[var(--gr-fg)]">
          Selection summary
        </div>
        <div class="text-sm text-[var(--gr-muted-fg)]">
          {{ enabledCount }} of 2 optional channels are active.
        </div>
      </div>

      <GrSwitch v-model="controlsDisabled" size="sm">
        Lock editable options
      </GrSwitch>
    </div>
  </div>
</template>

Интерактивное содержимое в слоте подписи

Отдельный сценарий фиксирует важную интеграционную деталь: ссылки и кнопки внутри slot-контента не должны случайно переключать чекбокс.

I accept the rollout policy and reviewed the privacy policy
Checkbox value: pending · Preview clicked 0 times.

Interactive Label
<script setup lang="ts">
import { ref } from 'vue'

import { GrCheckbox } from '@feugene/granularity'

const accepted = ref(false)
const previewOpens = ref(0)
</script>

<template>
  <div class="grid gap-4">
    <GrCheckbox v-model="accepted">
      <span class="inline-flex flex-wrap items-center gap-2 text-sm">
        I accept the rollout policy and reviewed the
        <a
          class="font-medium text-[var(--gr-primary-text)] underline underline-offset-2"
          href="https://example.com/policy"
          target="_blank"
          rel="noreferrer"
          @click.stop
        >
          privacy policy
        </a>
        <button
          type="button"
          class="rounded-full border border-[var(--gr-brd)] px-2 py-1 text-xs font-medium text-[var(--gr-fg)] transition hover:border-[var(--gr-primary)] hover:text-[var(--gr-primary-text)]"
          @click.stop="previewOpens += 1"
        >
          Preview changes
        </button>
      </span>
    </GrCheckbox>

    <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)]">
      Checkbox value: <span class="font-semibold text-[var(--gr-fg)]">{{ accepted ? 'accepted' : 'pending' }}</span> ·
      Preview clicked {{ previewOpens }} times.
    </div>
  </div>
</template>

Семантика отправки нативной формы

Показываем, какие name/value пары реально уходят в FormData, чтобы поведение чекбокса было предсказуемо в обычных формах и без обвязки form-library.

Marketing updates
Beta feature updates
Submit the form to inspect native checkbox values.

Native Form
<script setup lang="ts">
import { ref } from 'vue'

import { GrButton, GrCheckbox } from '@feugene/granularity'

const marketing = ref(true)
const productUpdates = ref(false)
const submission = ref('Submit the form to inspect native checkbox values.')

function onSubmit(event: SubmitEvent): void {
  event.preventDefault()

  const formData = new FormData(event.currentTarget as HTMLFormElement)

  submission.value = JSON.stringify(Object.fromEntries(formData.entries()), null, 2)
}
</script>

<template>
  <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
    <form class="grid gap-3" @submit="onSubmit">
      <GrCheckbox v-model="marketing" name="marketing" value="enabled">
        Marketing updates
      </GrCheckbox>
      <GrCheckbox v-model="productUpdates" name="productUpdates" value="beta">
        Beta feature updates
      </GrCheckbox>

      <div class="flex items-center gap-3 pt-2">
        <GrButton type="submit" size="sm">Read form data</GrButton>
      </div>
    </form>

    <!-- tabindex: скроллящийся блок обязан быть достижим с клавиатуры,
         иначе его содержимое недоступно без мыши (axe: scrollable-region-focusable). -->
    <pre
      tabindex="0"
      aria-label="Submitted form data"
      class="overflow-x-auto rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-fg)] p-4 text-xs text-[var(--gr-bg)]"
    >{{ submission }}</pre>
  </div>
</template>

Доступность

Паттерн APG
checkbox
Клавиши
Space — переключить (в т.ч. из indeterminate → включено)

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

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