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
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
indeterminate
state. - Form management, data collection, and validation with Standard Schemas or native HTML5 validation.
- Support for orientation with
horizontal
andvertical
values. v-model
support 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
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
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>();
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>
79 collapsed lines
<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
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>();
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>
80 collapsed lines
<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
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>
62 collapsed lines
<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
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
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
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>
Usage
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
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
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
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>
API
Checkbox
Props
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
These are the properties in the object returned by the useCheckbox
composable.
Name | Type | Description |
---|---|---|
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | 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. |
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. | |
labelProps | Props for the label element. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors 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. |
Checkbox Group
These are the properties that can be passed to the useCheckboxGroup
composable.
Props
These are the properties in the object returned by the useCheckboxGroup
composable.
Name | Type | Description |
---|---|---|
description | Optional description text for the checkbox group. | |
dir | The text direction for the checkbox group. | |
disabled | Whether the checkbox group is disabled. | |
disableHtmlValidation | Whether HTML5 validation should be disabled for this checkbox group. | |
label | The label for the checkbox group. | |
modelValue | The current value of the checkbox group. | |
name | The name/path of the checkbox group. | |
readonly | Whether the checkbox group is readonly. | |
required | Whether the checkbox group is required. | |
schema | The validation schema for the checkbox group. |
Returns
These are the properties in the object returned by the useCheckboxGroup
composable.
Name | Type | Description |
---|---|---|
descriptionProps | Props for the description element. | |
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | Props for the error message element. |
errors | The errors for the field. | |
fieldValue | The value of the field. | |
groupProps | Props for the group element. | |
groupState | The state of the checkbox group. | |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isTouched | Whether the field is touched. | |
isValid | Whether the field is valid. | |
labelProps | Props for the label element. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors 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. | |
validityDetails | Validity details for the checkbox group. |