Checkboxes
Checkboxes allow the user to toggle the field state on (checked) or off (unchecked). They are mainly used in a couple of scenarios:
- A single checkbox: Used for binary choices, such as acknowledging terms and conditions in sign-up forms. Typically, this is represented by a boolean value.
- A checkbox group: Used to represent multiple choices, such as updating email marketing preferences. This can be represented by an array of values for each selected choice.
Checkboxes are more nuanced than that, and we’ve put a lot of thought into how they should behave as components.
Features
Section titled “Features”You can build checkboxes using either native HTML input[type="checkbox"] elements or other HTML elements. We provide behavior, state, and accessibility implementation for both cases with the same API and features.
Currently, the following features are implemented:
- Support for either
input[type="checkbox"]or any other HTML elements as a base element for the checkbox. - Automatic linking of labels, descriptions, and error messages to input and label elements with
aria-*attributes. - Custom checked/unchecked values.
- Support for the
indeterminatestate. - Form management, data collection, and validation with Standard Schemas or native HTML5 validation.
v-modelsupport for both single and grouped checkboxes.- Supported Keyboard features:
| Key | Description |
|---|---|
| ⎵Space | Selects the focused checkbox item. |
| ⇥Tab | Moves the focus to the next checkbox item or the next tab order element in the document. |
⇧Shift + ⇥Tab | Moves the focus to the previous checkbox item or the previous tab order element in the document. |
Anatomy
Section titled “Anatomy”Building a Checkbox component
Section titled “Building a Checkbox component”The useCheckbox composable provides the behavior, state, and accessibility implementation for checkbox items. Checkbox items can be built with either input[type="checkbox"] or custom HTML elements depending on your styling needs.
You can start by importing the useCheckbox composable and using it in your checkbox component.
import { CheckboxProps, useCheckbox } from '@formwerk/core';
const props = defineProps<CheckboxProps>();
const { labelProps, inputProps } = useCheckbox(props);The useCheckbox composable returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the DOM elements.
<input v-bind="inputProps" /><label v-bind="labelProps">{{ label }}</label>These bindings contain all the necessary attributes and event listeners to manage the checkbox state and behavior and to ensure support for assistive technologies.
With that out of the way, let’s see how you can build a checkbox component. There are two ways to do it.
With input as a base element
Section titled “With input as a base element”This is how you would build a custom checkbox item component using the useCheckbox composable.
With the native input being limited in terms of styling, you can consider hiding it visually and instead use a custom element in its place. Note that while the element is visually hidden, it will still be announced by assistive technologies. Alternatively, you can use any element as a base instead.
The .sr-only class is used to hide the input element from the visual layout but keep it accessible to assistive technologies. You should have a similar class in your styling solution or framework as this use-case is very common.
Another thing to note is we’ve placed the input element inside a label element to ensure that the checkbox is still focusable and that clicking on the label also toggles the checkbox.
<script setup lang="ts">import { type CheckboxProps, useCheckbox } from '@formwerk/core';
const props = defineProps<CheckboxProps<string>>();
const { labelProps, inputProps, errorMessage, errorMessageProps, isChecked } = useCheckbox(props);</script>
<template> <div class="checkbox-item"> <label v-bind="labelProps"> <input v-bind="inputProps" class="sr-only" />
<div class="checkbox-square"> <svg v-if="isChecked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="fill-emerald-500" > <path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z" ></path> </svg> </div>
{{ label }} </label>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error"> {{ errorMessage }} </div> </div></template>
<style scoped>.checkbox-item { --color-primary: #10b981; --color-focus: var(--color-primary); --color-white: #fff; --color-gray-100: #fafafa; --color-gray-400: #a1a1aa; --color-error: #f00;
.checkbox-square { width: 1rem; height: 1rem; border-radius: 0.375rem; flex-shrink: 0; border: 1px solid var(--color-gray-400); display: flex; align-items: center; justify-content: center; background-color: var(--color-white); }
svg { width: 0.9rem; height: 0.9rem; fill: var(--color-white); }
label { display: inline-flex; gap: 4px; width: max-content; align-items: center; user-select: none; font-size: 14px; cursor: pointer; font-weight: 500;
&:hover { }
&:has(:focus) { .checkbox-square { border-color: var(--color-focus); outline: 1px solid var(--color-focus); background-color: var(--color-gray-100); } }
&:has(:checked) { .checkbox-square { background-color: var(--color-primary); border-color: var(--color-primary); } }
&:has(:disabled) { opacity: 0.5; cursor: not-allowed; } }
.error { color: var(--color-error); font-size: 13px; }}
/** This is a common utility CSS class, you can find it in your CSS framework of choice */.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0;}</style>With other HTML elements as a base
Section titled “With other HTML elements as a base”For special styling needs, you don’t have to use the input element. You can use a custom HTML element as a base with the same API.
<script setup lang="ts">import { type CheckboxProps, useCheckbox } from '@formwerk/core';
const props = defineProps<CheckboxProps<string>>();
const { labelProps, inputProps, errorMessage, errorMessageProps, isChecked } = useCheckbox(props);</script>
<template> <div class="checkbox-item"> <div v-bind="inputProps" class="checkbox-input"> <div class="checkbox-square"> <svg v-if="isChecked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="fill-emerald-500" > <path d="M232.49,80.49l-128,128a12,12,0,0,1-17,0l-56-56a12,12,0,1,1,17-17L96,183,215.51,63.51a12,12,0,0,1,17,17Z" ></path> </svg> </div>
<div v-bind="labelProps">{{ label }}</div> </div>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error"> {{ errorMessage }} </div> </div></template>
<style scoped>.checkbox-item { --color-primary: #10b981; --color-focus: var(--color-primary); --color-white: #fff; --color-gray-100: #fafafa; --color-gray-400: #a1a1aa; --color-error: #f00;
.checkbox-square { width: 1rem; height: 1rem; border-radius: 0.375rem; flex-shrink: 0; border: 1px solid var(--color-gray-400); display: flex; align-items: center; justify-content: center; background-color: var(--color-white); }
svg { width: 0.9rem; height: 0.9rem; fill: var(--color-white); }
.checkbox-input { display: inline-flex; gap: 4px; width: max-content; align-items: center; user-select: none; font-size: 14px; cursor: pointer; font-weight: 500;
&:hover { }
&:focus { outline: none; .checkbox-square { border-color: var(--color-focus); outline: 1px solid var(--color-focus); background-color: var(--color-gray-100); } }
&[aria-checked='true'] { .checkbox-square { background-color: var(--color-primary); border-color: var(--color-primary); } }
&[aria-disabled='true'] { opacity: 0.5; cursor: not-allowed; } }
.error { color: var(--color-error); font-size: 13px; }}
/** This is a common utility CSS class, you can find it in your CSS framework of choice */.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0;}</style>Building a Checkbox Group component
Section titled “Building a Checkbox Group component”You will use the useCheckboxGroup composable to build a checkbox group component. The composable offers the behavior, state, and accessibility implementation for checkbox groups.
Assuming that you’ve already built a checkbox component using the useCheckbox composable as the two are tightly coupled, the following example shows how you can build a checkbox group component.
We will use a checkbox without an input element base just to show you that it does not matter what element you use as a base. The same behaviors will be provided for either.
<script setup lang="ts">import { type CheckboxGroupProps, useCheckboxGroup } from '@formwerk/core';
const props = defineProps<CheckboxGroupProps>();
const { groupProps, labelProps, descriptionProps, errorMessageProps, errorMessage, isTouched,} = useCheckboxGroup(props);</script>
<template> <div v-bind="groupProps" class="checkbox-group" :class="{ 'is-touched': isTouched }" > <div v-bind="labelProps" class="group-label">{{ label }}</div>
<div class="checkboxes-container"> <slot /> </div>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error"> {{ errorMessage }} </div>
<div v-else-if="description" v-bind="descriptionProps" class="hint"> {{ description }} </div> </div></template>
<style scoped>.checkbox-group { --color-text: #333; --color-hint: #666; --color-border: #d4d4d8; --color-focus: #007bff; --color-error: #f00; --color-valid: #059669; --color-hover: #eee;
display: flex; flex-direction: column;
.hint, .error { margin-top: 0.25rem; }
.error { color: var(--color-error); display: none; font-size: 13px; }
.hint { color: var(--color-hint); font-size: 13px; opacity: 0; transition: opacity 0.3s ease; }
.checkboxes-container { margin-top: 0.25rem; display: flex; gap: 0.5rem; }
.group-label { color: var(--color-text); display: block; margin-bottom: 0.25rem; font-size: 14px; font-weight: 500; }
&:has(:focus) { .hint { opacity: 1; } }
&.is-touched:has(:invalid) { .error { display: block; }
.hint { display: none; } }}</style>Validation
Section titled “Validation”Checkbox components and checkbox groups can be validated by using Standard Schemas or native HTML5 validation via the schema prop on each of them.
When checkboxes are a part of a checkbox group, they will not report their own error messages as the group component should be responsible for displaying the error message.
HTML Constraints
Section titled “HTML Constraints”The following properties are supported on both composables if you use the input element as a base for the checkbox.
| Name | Type | Description |
|---|---|---|
required | boolean | Whether the checkbox or group is required. |
Here is an example of how to use the required property on either a CheckboxItem or a CheckboxGroup.
<script setup lang="ts">import { ref } from 'vue';import Checkbox from './Checkbox.vue';import CheckboxGroup from './CheckboxGroup.vue';</script>
<template> <!-- Single checkbox --> <Checkbox label="Terms" required />
<!-- Checkbox group --> <CheckboxGroup name="test" label="Colors" required> <Checkbox label="Red" value="red" /> <Checkbox label="Green" value="green" /> <Checkbox label="Blue" value="blue" /> </CheckboxGroup></template>Note that marking any checkbox item that is part of a checkbox group as required will not make the group required and it will be ignored.
Standard Schema
Section titled “Standard Schema”Both useCheckbox and useCheckboxGroup support Standard Schema validation through the schema prop. This includes multiple providers like Zod, Valibot, Arktype, and more.
<script setup lang="ts">import { z } from 'zod';import Checkbox from './Checkbox.vue';import CheckboxGroup from './CheckboxGroup.vue';
const schema = z.array(z.string()).min(1, 'Required');
const singleSchema = z.literal(true, 'Required');</script>
<template> <!-- Single checkbox --> <Checkbox label="Terms" :value="true" :schema="singleSchema" />
<!-- Checkbox group --> <CheckboxGroup label="Colors (Standard Schema)" :schema="schema"> <Checkbox label="Red" value="red" /> <Checkbox label="Green" value="green" /> <Checkbox label="Blue" value="blue" /> </CheckboxGroup></template>Disabled
Section titled “Disabled”You can disable individual checkboxes or the whole group with the disabled prop on either. Disabled checkboxes are not focusable. Disabled groups are not submitted and are not validated.
<script setup lang="ts">import Checkbox from './Checkbox.vue';import CheckboxGroup from './CheckboxGroup.vue';</script>
<template> <CheckboxGroup label="Checkbox Group"> <Checkbox label="Option 1" value="1" /> <Checkbox label="Option 2" value="2" /> <Checkbox label="Option 3" value="3" disabled /> <Checkbox label="Option 4" value="4" /> </CheckboxGroup>
<CheckboxGroup label="Disabled Group" disabled> <Checkbox label="Option 1" value="1" /> <Checkbox label="Option 2" value="2" /> <Checkbox label="Option 3" value="3" /> </CheckboxGroup></template>If you need to prevent the user from interacting with the checkboxes while still allowing it to submit, consider using readonly instead.
Readonly
Section titled “Readonly”Only available on the group and group-less checkboxes. The readonly prop prevents the user from interacting with the group/checkbox while still allowing it to submit and be validated.
<script setup lang="ts">import Checkbox from './Checkbox.vue';import CheckboxGroup from './CheckboxGroup.vue';</script>
<template> <CheckboxGroup label="Checkbox Group"> <Checkbox label="Option 1" value="1" /> <Checkbox label="Option 2" value="2" /> <Checkbox label="Option 3" value="3" readonly /> <Checkbox label="Option 4" value="4" /> </CheckboxGroup>
<CheckboxGroup label="Readonly Group" readonly> <Checkbox label="Option 1" value="1" /> <Checkbox label="Option 2" value="2" /> <Checkbox label="Option 3" value="3" /> </CheckboxGroup></template>Indeterminate
Section titled “Indeterminate”Only available on the checkbox item. The indeterminate state is used to model a tri-state checkbox. One of the situations it is used in is when a checkbox group has a mix of checked and unchecked items.
Checkboxes that are indeterminate will change their checked state until the indeterminate state is removed.
<script setup lang="ts">import { ref } from 'vue';import Checkbox from './Checkbox.vue';</script>
<template> <Checkbox label="Terms" indeterminate /></template>Group Checked State
Section titled “Group Checked State”The overall checked state of a group is reported via the groupState property on the useCheckboxGroup composable. The groupState property is an enum that represents if the group is entirely checked, unchecked, or mixed.
We will use a few SVGs to represent the group state visually.
<script setup lang="ts">import Checkbox from './Checkbox.vue';import { type CheckboxGroupProps, useCheckboxGroup } from '@formwerk/core';
const props = defineProps<CheckboxGroupProps>();
const { groupProps, groupState, labelProps } = useCheckboxGroup(props);
function toggleGroupState() { groupState.value = groupState.value === 'checked' ? 'unchecked' : 'checked';}</script>
<template> <div v-bind="groupProps"> <div v-bind="labelProps" class="group-label"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="#000000" viewBox="0 0 256 256" @click="toggleGroupState" > <path v-if="groupState === 'checked'" d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm-34.34,77.66-56,56a8,8,0,0,1-11.32,0l-24-24a8,8,0,0,1,11.32-11.32L112,148.69l50.34-50.35a8,8,0,0,1,11.32,11.32Z" ></path> <path v-else-if="groupState === 'mixed'" d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM168,136H88a8,8,0,0,1,0-16h80a8,8,0,0,1,0,16Z" ></path> <path v-else d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,176H48V48H208V208Z" ></path> </svg>
{{ label }} </div>
<div class="checkboxes"> <slot /> </div>
State: {{ groupState }} </div></template>
<style>.group-label { display: flex; gap: 4px; align-items: center;}
.checkboxes { margin-left: 1rem;}</style>Checkbox Value Type and Generics
Section titled “Checkbox Value Type and Generics”By default, the value of a checkbox is a boolean. You can change the value type by passing in the generic type to the CheckboxProps or CheckboxGroupProps types.
The following example shows how to change the value type to a string.
import { useCheckbox, type CheckboxProps } from '@formwerk/core';
const props = defineProps<CheckboxProps<string>>();
// ...But ideally, to make sure your component is type-safe and as flexible as possible, you should use generics on the component itself.
<script setup lang="ts" generic="TValue">import { useCheckbox, type CheckboxProps } from '@formwerk/core';
const props = defineProps<CheckboxProps<TValue>>();
//...</script>You can do the same for the CheckboxGroup components as well.
<script setup lang="ts" generic="TValue">import { useCheckboxGroup, type CheckboxGroupProps } from '@formwerk/core';
const props = defineProps<CheckboxGroupProps<TValue>>();
// ...</script>Checkbox
Section titled “Checkbox”These are the properties that can be passed to the useCheckbox composable.
| Name | Type | Description |
|---|---|---|
| disabled | Whether the checkbox is disabled. | |
| disableHtmlValidation | Whether HTML5 validation should be disabled for this checkbox. | |
| falseValue | The value to use when unchecked. | |
| indeterminate | Whether the checkbox is in an indeterminate state. | |
| label | The label for the checkbox. | |
| modelValue | The current value of the checkbox. | |
| name | The name/path of the checkbox field. | |
| readonly | Whether the checkbox is readonly. | |
| required | Whether the checkbox is required. | |
| schema | The validation schema for the checkbox. | |
| standalone | Whether the checkbox should operate independently of any checkbox group. | |
| trueValue | The value to use when checked. | |
| value | The value to use when part of a checkbox group. |
Returns
Section titled “Returns”These are the properties in the object returned by the useCheckbox composable.
| Name | Type | Description |
|---|---|---|
| controlId | The id of the input element. | |
| descriptionProps | Props for the description element. | |
| errorMessage | The error message for the field. | |
| errorMessageProps | Props for the error message element. | |
| errors | The errors for the field. | |
| fieldValue | The value of the field. | |
| inputEl | Reference to the input element. | |
| inputProps | Ref<CheckboxDomInputProps | CheckboxDomProps> | Props for the input element. |
| isBlurred | Whether the field is blurred. | |
| isChecked | Whether the checkbox is checked. | |
| isDirty | Whether the field is dirty. | |
| isDisabled | Whether the field is disabled. | |
| isGrouped | Whether the checkbox is grouped. | |
| isTouched | Whether the field is touched. | |
| isValid | Whether the field is valid. | |
| isValidated | Whether the field is validated, used to determine whether to show validation errors or not. | |
| labelProps | Props for the label element. | |
| setBlurred | Sets the blurred state for the field. | |
| setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
| setIsValidated | Sets the validated state for the field. | |
| setTouched | Sets the touched state for the field. | |
| setValue | Sets the value for the field. | |
| submitErrorMessage | The error message for the field from the last submit attempt. | |
| submitErrors | The errors for the field from the last submit attempt. | |
| toggle | Toggles the value of the checkbox. | |
| validate | (mutate?: boolean) => Promise<ValidationResult<unknown>> | Validates the field. |
Checkbox Group
Section titled “Checkbox Group”These are the properties that can be passed to the useCheckboxGroup composable.
These are the properties in the object returned by the useCheckboxGroup composable.
| Name | Type | Description |
|---|
Returns
Section titled “Returns”These are the properties in the object returned by the useCheckboxGroup composable.
| Name | Type | Description |
|---|---|---|
| descriptionProps | Props for the description element. | |
| errorMessage | The error message for the field. | |
| errorMessageProps | Props for the error message element. | |
| errors | The errors for the field. | |
| fieldValue | The value of the field. | |
| groupId | The id of the group element. | |
| groupProps | Props for the group element. | |
| groupState | The state of the checkbox group. | |
| isBlurred | Whether the field is blurred. | |
| isDirty | Whether the field is dirty. | |
| isDisabled | Whether the field is disabled. | |
| isTouched | Whether the field is touched. | |
| isValid | Whether the field is valid. | |
| isValidated | Whether the field is validated, used to determine whether to show validation errors or not. | |
| labelProps | Props for the label element. | |
| setBlurred | Sets the blurred state for the field. | |
| setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
| setIsValidated | Sets the validated state for the field. | |
| setTouched | Sets the touched state for the field. | |
| setValue | Sets the value for the field. | |
| submitErrorMessage | The error message for the field from the last submit attempt. | |
| submitErrors | The errors for the field from the last submit attempt. | |
| validate | (mutate?: boolean) => Promise<ValidationResult<unknown>> | Validates the field. |