<script setup lang="ts">
import { ref } from 'vue'
import { CheckboxGroup } from '@hina-ui/vue'
const types = ref(['gal', 'ln'])
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: 'Light novel' },
{ value: 'manga', label: 'Manga' },
]
</script>
<template>
<CheckboxGroup v-model="types" :options="options" aria-label="Work types" />
</template>
Usage
import { CheckboxGroup } from '@hina-ui/vue'
The group renders one Checkbox per entry in options, and v-model binds the array of chosen values; the option type is the same as Select’s. Attributes it does not declare land on the root, so name the group with aria-label or aria-labelledby.
Value: [ "mail" ]
<script setup lang="ts">
import { ref } from 'vue'
import { CheckboxGroup, Stack, Text } from '@hina-ui/vue'
const channels = ref(['mail'])
const options = [
{ value: 'mail', label: 'Email' },
{ value: 'push', label: 'Push notification' },
{ value: 'sms', label: 'SMS' },
]
</script>
<template>
<Stack gap="sm">
<CheckboxGroup v-model="channels" :options="options" aria-label="Notification channels" />
<Text size="sm" tone="muted">Value: {{ channels }}</Text>
</Stack>
</template>
Examples
Horizontal
With orientation="horizontal" the options run in a row and wrap when they run out of room.
<script setup lang="ts">
import { ref } from 'vue'
import { CheckboxGroup } from '@hina-ui/vue'
const days = ref(['sat', 'sun'])
const options = [
{ value: 'mon', label: 'Mon' },
{ value: 'wed', label: 'Wed' },
{ value: 'fri', label: 'Fri' },
{ value: 'sat', label: 'Sat' },
{ value: 'sun', label: 'Sun' },
]
</script>
<template>
<CheckboxGroup
v-model="days"
:options="options"
orientation="horizontal"
aria-label="Release days"
/>
</template>
Description
An option’s description shows under its label.
<script setup lang="ts">
import { ref } from 'vue'
import { CheckboxGroup } from '@hina-ui/vue'
const scopes = ref(['read'])
const options = [
{ value: 'read', label: 'Read', description: 'View your library and reading progress' },
{ value: 'write', label: 'Write', description: 'Change your library and ratings' },
{ value: 'admin', label: 'Manage', description: 'Manage tokens and granted scopes' },
]
</script>
<template>
<CheckboxGroup v-model="scopes" :options="options" aria-label="Scopes" />
</template>
Settings rows
Use control-placement="end" to place the control after the text and block to fill the available width. The description stays below the label. start and end follow the text direction. With block, vertical items fill each row; horizontal items share the row equally. orientation still controls the arrangement of options.
<script setup lang="ts">
import { ref } from 'vue'
import { CheckboxGroup } from '@hina-ui/vue'
const scopes = ref(['read'])
const options = [
{ value: 'read', label: 'Read', description: 'View your library and reading progress' },
{ value: 'write', label: 'Write', description: 'Change your library and ratings' },
{ value: 'admin', label: 'Manage', description: 'Manage tokens and granted scopes' },
]
</script>
<template>
<CheckboxGroup
v-model="scopes"
control-placement="end"
block
:options="options"
aria-label="Scopes"
/>
</template>
Sizes
size is passed to every checkbox.
<script setup lang="ts">
import { CheckboxGroup, Inline } from '@hina-ui/vue'
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: 'Light novel' },
]
</script>
<template>
<Inline gap="lg" align="start">
<CheckboxGroup size="sm" :options="options" :model-value="['gal']" aria-label="Small" />
<CheckboxGroup size="md" :options="options" :model-value="['gal']" aria-label="Medium" />
<CheckboxGroup size="lg" :options="options" :model-value="['gal']" aria-label="Large" />
</Inline>
</template>
States
disabled on an option disables that option only; on the group it disables them all. invalid reaches every box.
<script setup lang="ts">
import { CheckboxGroup, Inline } from '@hina-ui/vue'
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: 'Light novel' },
{ value: 'manga', label: 'Manga', disabled: true },
]
</script>
<template>
<Inline gap="lg" align="start">
<CheckboxGroup :options="options" :model-value="['gal']" aria-label="With a disabled option" />
<CheckboxGroup disabled :options="options" :model-value="['gal']" aria-label="Disabled group" />
<CheckboxGroup invalid :options="options" :model-value="[]" aria-label="Invalid" />
</Inline>
</template>
Custom content
The option slot customises each label.
The complete option type is inferred from options. Slot parameters preserve additional fields and their types; v-model still binds to an array of option values. See Select for the type definitions.
<script setup lang="ts">
import { ref } from 'vue'
import { BookOpen, Gamepad2, Images } from '@lucide/vue'
import { CheckboxGroup, Inline } from '@hina-ui/vue'
const icons = { gal: Gamepad2, ln: BookOpen, manga: Images }
const types = ref(['gal'])
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: 'Light novel' },
{ value: 'manga', label: 'Manga' },
]
</script>
<template>
<CheckboxGroup v-model="types" :options="options" aria-label="Work types">
<template #option="{ option }">
<Inline as="span" gap="xs" align="center">
<component :is="icons[option.value as keyof typeof icons]" class="text-muted size-4" />
{{ option.label }}
</Inline>
</template>
</CheckboxGroup>
</template>
In a form
Inside a FormField the label is linked to the whole group through aria-labelledby, and the description and error message are rendered by the field; validation rules and submission belong to the Form.
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, CheckboxGroup, Form, FormField, Text } from '@hina-ui/vue'
const options = [
{ label: 'Games', value: 'game' },
{ label: 'Novels', value: 'novel' },
{ label: 'Manga', value: 'manga' },
{ label: 'Anime', value: 'anime' },
]
const schema = v.object({
interests: v.pipe(
v.array(v.string('Pick your interests')),
v.minLength(1, 'Pick at least one'),
v.maxLength(2, 'Pick at most two'),
),
})
const values = reactive({ interests: [] as Array<string | number> })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
<FormField name="interests" label="Interests" description="One or two of them" required>
<CheckboxGroup v-model="values.interests" :options="options" />
</FormField>
<Button type="submit" :loading="submitting" class="self-start">Save</Button>
<Text v-if="saved" tone="muted" size="sm">Saved: {{ saved }}</Text>
</Form>
</template>
Behaviour
- Ticking adds the value to the array, ticking again removes it; the array keeps the order values were ticked in.
- Every checkbox is its own Tab stop, Space toggles the focused one and the arrow keys do not move focus, as with a native group of checkboxes.
Accessibility
- The root is
role="group", named witharia-labeloraria-labelledby; each item keeps theCheckboxsemantics.
API
Props
T extends SelectOption is inferred from options and defaults to SelectOption.
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | Array<string | number> | [] | The chosen values |
options | T[] | — | The items, see Select |
orientation | 'vertical' | 'horizontal' | 'vertical' | Layout direction |
size | 'sm' | 'md' | 'lg' | 'md' | Size of every checkbox |
controlPlacement | 'start' | 'end' | 'start' | Position of the control relative to its text |
block | boolean | false | Fill the group; horizontal items share the width |
disabled | boolean | false | Whether the whole group is disabled |
invalid | boolean | false | Whether the group failed validation |
class | string | — | Classes appended to the root element |
Slots
| Slot | Payload | Description |
|---|---|---|
option | { option: T } | Label of each item |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: Array<string | number> | The chosen values changed |