CCheckbox
CCheckbox is a checkbox built on top of CInput and CSelectControl. It wraps a real <input type="checkbox">, so focus, keyboard and screen-reader behaviour are the native ones. It binds a boolean or collects values into an array, supports an indeterminate state, and takes part in CForm validation.
Any CInput prop — label, details, no-details, rules, validate-on, disabled, readonly, preset — is forwarded to the underlying input.
Basic usage
Bind a boolean with v-model and pass a label.
Show code
<template>
<c-checkbox v-model="subscribed" label="Email me about releases" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const subscribed = ref(true)
</script>Groups
Point several checkboxes at the same array model and give each one a value. Checking a box appends its value to the array, unchecking removes it — no group wrapper needed.
Show code
<template>
<c-checkbox
v-for="scope in available"
:key="scope.value"
v-model="scopes"
:value="scope.value"
:label="scope.label"
no-details
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const scopes = ref(['read'])
const available = [
{ value: 'read', label: 'Read repositories' },
{ value: 'write', label: 'Write repositories' },
{ value: 'admin', label: 'Manage members' },
]
</script>Color
color sets the box colour through a text utility (text-{color}), so it accepts palette tokens and raw CSS colours.
A raw colour must be a literal
Arbitrary classes (text-[#e65100]) are generated through a static source scan: color="#e65100" works, :color="someVar" with a raw value does not. Palette tokens are unaffected.
Show code
<template>
<c-checkbox
v-for="item in variants"
:key="item.value"
v-model="selected"
:value="item.value"
:label="item.label"
:color="item.color"
no-details
/>
<c-checkbox
v-model="selected"
value="custom"
label="Custom CSS color"
color="#e65100"
no-details
/>
<c-checkbox
v-model="selected"
value="brand"
label="Brand variable"
color="var(--c-sys-color-primary)"
no-details
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const selected = ref(['teal', 'rose', 'amber', 'custom'])
const variants = [
{ value: 'teal', label: 'Teal', color: 'teal' },
{ value: 'indigo', label: 'Indigo', color: 'indigo' },
{ value: 'rose', label: 'Rose', color: 'pink-darken-1' },
{ value: 'amber', label: 'Amber', color: 'amber-darken-2' },
{ value: 'green', label: 'Green', color: 'green-darken-1' },
{ value: 'cyan', label: 'Cyan', color: 'cyan-darken-2' },
{ value: 'purple', label: 'Purple', color: 'deep-purple-lighten-1' },
{ value: 'red', label: 'Red', color: 'red-darken-1' },
]
</script>Size
size changes only the box size, leaving the row height and label typography alone. Numbers become pixels, while strings are passed through as CSS values.
Show code
<template>
<c-checkbox
v-for="item in variants"
:key="item.value"
v-model="selected"
:value="item.value"
:label="item.label"
:size="item.size"
color="indigo"
no-details
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const selected = ref(['compact'])
const variants = [
{ value: 'compact', label: 'Compact 16', size: 16 },
{ value: 'regular', label: 'Regular 20', size: 20 },
{ value: 'comfortable', label: 'Comfortable 24', size: 24 },
{ value: 'large', label: 'Large 28', size: '28px' },
]
</script>Indeterminate
indeterminate renders the third state and sets the native indeterminate property on the input, so assistive tech reports the checkbox as mixed. It is independent of modelValue: a checkbox can be unchecked and mixed at the same time.
Toggling clears it — the component emits update:indeterminate with false. In the typical "select all" case the parent state is fully derived from the children, so a one-way :indeterminate binding is enough.
Show code
<template>
<c-checkbox v-model="allInvited" :indeterminate="someInvited" label="All teams" no-details />
<c-checkbox
v-for="team in teams"
:key="team"
v-model="invited"
:value="team"
:label="team"
no-details
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
const teams = ['Design', 'Engineering', 'Marketing']
const invited = ref(['Design'])
const allInvited = computed({
get: () => invited.value.length === teams.length,
set: (value: boolean) => {
invited.value = value ? [...teams] : []
},
})
const someInvited = computed(() => invited.value.length > 0 && invited.value.length < teams.length)
</script>States and validation
rules receive the model value — the boolean, or the array in group mode. With validate-on="blur" validation runs once the checkbox loses focus.
Show code
<template>
<c-checkbox
v-model="terms"
label="I accept the terms of service"
:rules="termsRules"
validate-on="blur"
details="Validation runs on blur"
>
<template #details="{ errorMessage, details }">
<span :class="{ error: !!errorMessage }">{{ errorMessage || details }}</span>
</template>
</c-checkbox>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const terms = ref(false)
const termsRules = [(value?: boolean) => ({ valid: !!value, message: 'You must accept the terms' })]
</script>Slots
The default slot replaces the label text and receives the current state; icon replaces the box itself.
Show code
<template>
<c-checkbox v-model="starred" no-details>
<template #icon="{ checked }">
<c-icon
name="fas:star"
source="fa"
:size="18"
:class="checked ? 'text-amber' : 'text-blue-grey-lighten-2'"
/>
</template>
Star this repository
</c-checkbox>
<c-checkbox v-model="accepted" no-details>
<template #default="{ checked }">
<span class="d-flex items-center gap-2">
Watch releases
<c-chip v-if="checked" class="bg-indigo text-white fs-xs">on</c-chip>
</span>
</template>
</c-checkbox>
</template>The label is not a click target of its own — the native input covers the whole component, which is what makes the entire row clickable. Interactive elements (links, buttons) placed inside the slots will not receive clicks; put them next to the checkbox instead.
Presets
The checkbox reads its preset from the input set: the nested checkbox field of the base snapshot takes a plain CCheckboxPreset, the same way field takes a CFieldPreset. Zones are root, icon and label; states collapse in the order disabled > readonly > error > focused > indeterminate > checked. For checkboxes, focused applies only on :focus-visible.
Show code
import type { CInputPreset } from '@vueland/ui/types'
const consent: CInputPreset = {
base: {
checkbox: {
base: { icon: ['text-blue-grey'], label: ['text-blue-grey'] },
checked: { icon: ['text-indigo'], label: ['text-indigo', 'fw-semi-bold'] },
indeterminate: { icon: ['text-indigo'] },
focused: { icon: ['text-indigo-darken-2'] },
error: { icon: ['text-red'], label: ['text-red'] },
disabled: { icon: ['text-grey-lighten-1'], label: ['text-grey-lighten-1'] },
},
},
error: { details: ['text-red'] },
}<template>
<c-checkbox v-model="allEnabled" label="All channels" preset="input.consent" />
</template>Behaviour
- Keyboard — the native input handles it:
Tabfocuses,Spacetoggles. - Readonly — the checkbox stays focusable and keeps its value visible, but toggling is cancelled and the model never changes. It is exposed as
aria-readonly, since a native checkbox has no readonly of its own. - Disabled — blocks focus and changes, and rules are not evaluated.
- Animation — checking fills the box and draws the tick with
stroke-dashoffset; the indeterminate dash scales and rotates in. Both use the system motion tokens and collapse to instant underprefers-reduced-motion: reduce.
Styling
The box is not an icon: it is a CSS border, while the tick and dash take their geometry from the checkboxCheckMark and checkboxIndeterminateMark aliases and render inside one CIcon SVG layer as centreline paths. That is what makes the tick drawable — a filled glyph from an icon set has no centreline to run a dash along, so checkboxOn and checkboxIndeterminate play no part here. Use the icon slot to replace the box entirely.
Overriding those aliases through icons.aliases reskins the marks app-wide. Supply centreline paths, not silhouettes — with a filled glyph the dash traces the outline instead of drawing the mark. Both marks share one SVG whose viewBox comes from the check-mark entry, so author them in the same coordinate system.
Two custom properties are exposed on .c-checkbox__icon:
--c-checkbox-size20pxsize prop sets this--c-checkbox-mark-color--c-sys-color-on-primaryThe box itself is painted with currentColor, so colour utilities and preset zones drive it.
API
Props
modelValueT | T[] | booleanfalsevalues in group modevalueTindeterminatebooleanfalsecolorstringsizenumber | string20px--c-checkbox-sizedisabledbooleanfalsereadonlybooleanfalseAny CInput prop — label, details, no-details, rules, validate-on, preset, … — is forwarded to the underlying input.
Events
update:modelValueT | T[] | booleanupdate:indeterminatebooleanfalse when the checkbox is toggledSlots
default{ checked: boolean, indeterminate: boolean }label texticon{ checked: boolean, indeterminate: boolean }details{ errorMessage?: string, details?: string, hasError: boolean }