Skip to content

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.

Aria Patterns

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 and vertical values.
  • v-model support for both single and grouped checkboxes.
  • Supported Keyboard features:
KeyDescription
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

Input
I accept the terms
Checkbox Label
Choose Drink
Group Label
Tea
Coffee
Input
Water
Checkbox Label
Checkbox Group

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 { ref } from 'vue';
import Checkbox from './Checkbox.vue';
const isChecked = ref(false);
</script>
<template>
<Checkbox label="Terms" v-model="isChecked" />
checked: {{ isChecked }}
</template>
<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 { ref } from 'vue';
import Checkbox from './Checkbox.vue';
const isChecked = ref(false);
</script>
<template>
<Checkbox label="Terms" v-model="isChecked" />
checked: {{ isChecked }}
</template>
<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 { ref } from 'vue';
import Checkbox from './Checkbox.vue';
import CheckboxGroup from './CheckboxGroup.vue';
const colors = ref([]);
</script>
<template>
<CheckboxGroup label="Colors" v-model="colors">
<Checkbox label="Red" value="red" />
<Checkbox label="Green" value="green" />
<Checkbox label="Blue" value="blue" />
</CheckboxGroup>
selected colors: {{ colors }}
</template>
<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.

NameTypeDescription
requiredbooleanWhether 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 { ref } from 'vue';
import Checkbox from './Checkbox.vue';
import CheckboxGroup from './CheckboxGroup.vue';
</script>
<template>
<CheckboxGroup label="Colors" v-model="colors">
<Checkbox label="Red" value="red" />
<Checkbox label="Green" value="green" />
<Checkbox label="Blue" value="blue" />
</CheckboxGroup>
</template>
<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.

NameTypeDescription
disabledWhether the checkbox is disabled.
disableHtmlValidationWhether HTML5 validation should be disabled for this checkbox.
falseValueThe value to use when unchecked.
indeterminateWhether the checkbox is in an indeterminate state.
labelThe label for the checkbox.
modelValueThe current value of the checkbox.
nameThe name/path of the checkbox field.
readonlyWhether the checkbox is readonly.
requiredWhether the checkbox is required.
schemaThe validation schema for the checkbox.
standaloneWhether the checkbox should operate independently of any checkbox group.
trueValueThe value to use when checked.
valueThe value to use when part of a checkbox group.

Returns

These are the properties in the object returned by the useCheckbox composable.

NameTypeDescription
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
Props for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
inputElReference to the input element.
inputProps
Ref<CheckboxDomInputProps | CheckboxDomProps>
Props for the input element.
isCheckedWhether the checkbox is checked.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isGroupedWhether the checkbox is grouped.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsProps for the label element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
toggleToggles 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.

NameTypeDescription
descriptionOptional description text for the checkbox group.
dirThe text direction for the checkbox group.
disabledWhether the checkbox group is disabled.
disableHtmlValidationWhether HTML5 validation should be disabled for this checkbox group.
labelThe label for the checkbox group.
modelValueThe current value of the checkbox group.
nameThe name/path of the checkbox group.
readonlyWhether the checkbox group is readonly.
requiredWhether the checkbox group is required.
schemaThe validation schema for the checkbox group.

Returns

These are the properties in the object returned by the useCheckboxGroup composable.

NameTypeDescription
descriptionPropsProps for the description element.
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
Props for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
groupPropsProps for the group element.
groupStateThe state of the checkbox group.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsProps for the label element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
validityDetailsValidity details for the checkbox group.