GrCheckboxGroup
Collects checkboxes into one multi-select field with a shared model and states.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- several values out of a short set — permissions, tags, days of the week, notification channels;
- every option has to be visible — the selection is seen as a whole, without opening a panel;
- the group is part of a form — a shared
string[]model, shareddisabled,readonlyandinvalid; - there are up to a dozen options — beyond that the list grows longer than the screen.
When to take something else
| Need | Take |
|---|---|
| There are many options | GrSelect with multiple |
| There are many options and they are searched by typing | GrAutocomplete |
| Only one can be selected | GrRadioGroup |
| There is a single checkbox | GrCheckbox |
| The options are nested | GrTreeSelect |
The model and the context
The group hands the nested checkboxes the selected values, name, size and the
disabled/readonly/invalid states, so they need no v-model of their own —
value is enough:
<GrCheckboxGroup v-model="channels" name="channels" :options="options" />
<GrCheckboxGroup v-model="channels" direction="horizontal">
<GrCheckbox value="sms">SMS</GrCheckbox>
<GrCheckbox value="email">Email</GrCheckbox>
</GrCheckboxGroup>
name goes to every checked checkbox, so a native form gets a repeated field:
new FormData(form).getAll('channels').
The disabled of an individual option beats an enabled group. A value in the model
that is not among the options is not lost by the group: unchecking a neighbouring
checkbox keeps it.
The role and ARIA
role="group", not radiogroup: checkboxes have no roving tabindex and no moving
of the selection with the arrows — each stays a Tab stop of its own, exactly like
a set of native <input type="checkbox">.
role="group" does not support aria-required and aria-readonly (axe reports that
as a critical aria-allowed-attr), so both states are declared by the checkboxes
themselves. aria-invalid, on the contrary, hangs on the group alone: duplicated on
every item, it would make a screen reader repeat “invalid value” as many times as
there are checkboxes in the group — on an error the items are merely recoloured.
Being required
As with a single checkbox, required is a declaration; a form rule does the
checking. For a group the built-in required works: an empty array counts as an
empty value.
const rules: GrFormRules = {
channels: [{ required: true, message: 'Choose at least one channel' }],
}Playground 7
Loading…
<GrCheckboxGroup />Install
npm i @feugene/granularityImport
import { GrCheckboxGroup } from '@feugene/granularity/components/GrCheckboxGroup'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
options | GrCheckboxGroupOption[] | undefined | undefined | — |
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Read-only: the selection is visible but does not change. |
invalid | boolean | undefined | false | The visual and ARIA state of an error for the whole group. |
required | boolean | undefined | false | A required group: `aria-required` is announced by the checkboxes themselves. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | A shared name for a native form: the values leave as repeated fields. |
direction | GrCheckboxGroupDirection | undefined | "vertical" | — |
modelValuerequired | string[] | — | — |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | Markup of your own for the checkboxes instead of generating them from `options`. |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: string[]] | — |
change | [value: string[]] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Description |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Examples 2
Multi-select from an options list
<script setup lang="ts">
import { ref } from 'vue'
import { GrCheckboxGroup, GrSegmented } from '@feugene/granularity'
const options = [
{ value: 'email', label: 'Email' },
{ value: 'sms', label: 'SMS' },
{ value: 'push', label: 'Push' },
{ value: 'webhook', label: 'Webhook', disabled: true },
]
const channels = ref(['email', 'push'])
const direction = ref<'vertical' | 'horizontal'>('vertical')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_240px]">
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<GrCheckboxGroup
v-model="channels"
name="channels"
:options="options"
:direction="direction"
aria-label="Notification channels"
/>
</div>
<div class="grid gap-3 rounded-2xl border border-dashed border-[var(--gr-brd)] p-4">
<GrSegmented
v-model="direction"
size="sm"
:options="[
{ value: 'vertical', label: 'Vertical' },
{ value: 'horizontal', label: 'Horizontal' },
]"
/>
<div class="text-sm text-[var(--gr-muted-fg)]">
Selected: <span class="font-semibold text-[var(--gr-fg)]">{{ channels.join(', ') || 'none' }}</span>
</div>
</div>
</div>
</template>Validation inside GrForm
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { GrButton, GrCheckbox, GrCheckboxGroup, GrForm, GrFormField, type GrFormRules } from '@feugene/granularity'
const options = [
{ value: 'incidents', label: 'Incidents' },
{ value: 'releases', label: 'Releases' },
{ value: 'digest', label: 'Weekly digest' },
]
const model = reactive({ scopes: [] as string[], terms: false })
// `required` пустым считает `null`/`''`/`[]`, но не `false`: снятый чекбокс —
// это законное значение поля. «Согласие обязательно» — это `validator`.
const rules: GrFormRules = {
scopes: [{ required: true, message: 'Pick at least one subscription' }],
terms: [{ validator: value => value === true || 'Accept the policy to continue' }],
}
const submitted = ref(false)
</script>
<template>
<GrForm
:model="model"
:rules="rules"
class="grid max-w-md gap-4"
@submit="submitted = true"
>
<GrFormField name="scopes" label="Subscriptions">
<GrCheckboxGroup v-model="model.scopes" name="scopes" :options="options" required />
</GrFormField>
<GrFormField name="terms" label="Policy">
<GrCheckbox v-model="model.terms" required>
I accept the notification policy
</GrCheckbox>
</GrFormField>
<div class="flex gap-2">
<GrButton type="submit" size="sm">
Save preferences
</GrButton>
</div>
<p v-if="submitted" class="text-sm text-[var(--gr-success-text)]">
Saved — the form is valid.
</p>
</GrForm>
</template>Accessibility
- APG pattern
group