Checkbox
A box people tick to opt into one or several independent options.
'use client'
import { useState } from 'react'
import { Checkbox, CheckboxField, CheckboxGroup } from '@/components/checkbox'
import { Description, Fieldset, Label, Legend } from '@/components/fieldset'
import { Text } from '@/components/text'
export default function Example() {
let [finalized, setFinalized] = useState(true)
let [failed, setFailed] = useState(false)
return (
<Fieldset>
<Legend>Invoice emails</Legend>
<Text>Choose which billing events reach finance@northwind.io.</Text>
<CheckboxGroup>
<CheckboxField>
<Checkbox name="emails" value="invoice_finalized" checked={finalized} onChange={setFinalized} />
<Label>Invoice finalized</Label>
<Description>A PDF copy is attached as soon as the invoice gets its number.</Description>
</CheckboxField>
<CheckboxField>
<Checkbox name="emails" value="payment_failed" checked={failed} onChange={setFailed} />
<Label>Payment failed</Label>
<Description>Sent when a card is declined on a renewal.</Description>
</CheckboxField>
</CheckboxGroup>
</Fieldset>
)
}<script setup lang="ts">
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import CheckboxGroup from '@/components/checkbox-group.vue'
import Description from '@/components/description.vue'
import Fieldset from '@/components/fieldset.vue'
import Label from '@/components/label.vue'
import Legend from '@/components/legend.vue'
import Text from '@/components/text.vue'
import { ref } from 'vue'
const finalized = ref(true)
const failed = ref(false)
</script>
<template>
<Fieldset>
<Legend>Invoice emails</Legend>
<Text>Choose which billing events reach finance@northwind.io.</Text>
<CheckboxGroup>
<CheckboxField>
<Checkbox name="emails" value="invoice_finalized" v-model="finalized" />
<Label>Invoice finalized</Label>
<Description>A PDF copy is attached as soon as the invoice gets its number.</Description>
</CheckboxField>
<CheckboxField>
<Checkbox name="emails" value="payment_failed" v-model="failed" />
<Label>Payment failed</Label>
<Description>Sent when a card is declined on a renewal.</Description>
</CheckboxField>
</CheckboxGroup>
</Fieldset>
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import type React from 'react'
import { CheckIcon, MinusIcon } from '@heroicons/react/16/solid'
export function CheckboxGroup({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return (
<div
data-slot="control"
{...props}
className={clsx(
className,
'space-y-3 has-data-[slot=description]:space-y-6',
'has-data-[slot=description]:**:data-[slot=label]:font-medium'
)}
/>
)
}
const fieldGrid = 'grid gap-x-4 gap-y-1 grid-cols-[1.125rem_1fr] sm:grid-cols-[1rem_1fr]'
const fieldColumns = '*:data-[slot=control]:col-start-1 *:data-[slot=label]:col-start-2 *:data-[slot=description]:col-start-2'
const fieldRows = '*:data-[slot=control]:row-start-1 *:data-[slot=label]:row-start-1 *:data-[slot=description]:row-start-2'
const controlOffset = '*:data-[slot=control]:mt-0.75 sm:*:data-[slot=control]:mt-1'
export function CheckboxField({ className, ...props }: { className?: string } & Omit<Headless.FieldProps, 'as' | 'className'>) {
return (
<Headless.Field
data-slot="field"
{...props}
className={clsx(className, fieldGrid, fieldColumns, fieldRows, controlOffset, 'has-data-[slot=description]:**:data-[slot=label]:font-medium')}
/>
)
}
type CheckboxColors = {
mark: string
background: string
border: string
dark?: Partial<Omit<CheckboxColors, 'dark'>>
}
const withOpacity = (color: string, percent: number) => `color-mix(in oklab, ${color} ${percent}%, transparent)`
const palette = (hue: string) => (shade: number) => `var(--color-${hue}-${shade})`
const accent = (shade: number) => `var(--accent-${shade},var(--color-blue-${shade}))`
const zinc = palette('zinc')
const white = 'var(--color-white)'
function whiteMark(color: (shade: number) => string, background: number, border: number, opacity = 90): CheckboxColors {
return { mark: white, background: color(background), border: withOpacity(color(border), opacity) }
}
function darkMark(color: (shade: number) => string, background: number, border: number): CheckboxColors {
return { mark: color(950), background: color(background), border: withOpacity(color(border), 80) }
}
const darkCheckbox = whiteMark(zinc, 900, 950)
const whiteCheckbox = { mark: zinc(900), background: white, border: withOpacity(zinc(950), 15) }
const colors = {
'dark/zinc': { ...darkCheckbox, dark: { background: zinc(600) } },
'dark/white': { ...darkCheckbox, dark: whiteCheckbox },
white: whiteCheckbox,
dark: darkCheckbox,
zinc: whiteMark(zinc, 600, 700),
red: whiteMark(palette('red'), 600, 700),
orange: whiteMark(palette('orange'), 500, 600),
amber: darkMark(palette('amber'), 400, 500),
yellow: darkMark(palette('yellow'), 300, 400),
lime: darkMark(palette('lime'), 300, 400),
green: whiteMark(palette('green'), 600, 700),
emerald: whiteMark(palette('emerald'), 600, 700),
teal: whiteMark(palette('teal'), 600, 700),
cyan: darkMark(palette('cyan'), 300, 400),
sky: whiteMark(palette('sky'), 500, 600, 80),
blue: whiteMark(palette('blue'), 600, 700),
accent: whiteMark(accent, 600, 700),
indigo: whiteMark(palette('indigo'), 500, 600),
violet: whiteMark(palette('violet'), 500, 600),
purple: whiteMark(palette('purple'), 500, 600),
fuchsia: whiteMark(palette('fuchsia'), 500, 600),
pink: whiteMark(palette('pink'), 500, 600),
rose: whiteMark(palette('rose'), 500, 600),
} satisfies Record<string, CheckboxColors>
type Color = keyof typeof colors
function cssVariables(colors: CheckboxColors) {
return {
'--checkbox-mark': colors.mark,
'--checkbox-fill': colors.background,
'--checkbox-border': colors.border,
'--checkbox-dark-mark': colors.dark?.mark,
'--checkbox-dark-fill': colors.dark?.background,
'--checkbox-dark-border': colors.dark?.border,
} as React.CSSProperties
}
const themeColors = [
'[--mark-color:var(--checkbox-mark)] [--checked-background:var(--checkbox-fill)] [--checked-border:var(--checkbox-border)]',
'dark:[--mark-color:var(--checkbox-dark-mark,var(--checkbox-mark))]',
'dark:[--checked-background:var(--checkbox-dark-fill,var(--checkbox-fill))]',
'dark:[--checked-border:var(--checkbox-dark-border,var(--checkbox-border))]',
]
const layout = 'relative isolate flex items-center justify-center size-4.5 sm:size-4 rounded-[0.3125rem]'
const background = [
'before:absolute before:-z-10 before:inset-0 before:rounded-[calc(0.3125rem-1px)]',
'before:shadow-sm before:bg-white',
]
const highlight = [
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)]',
'after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
]
const border = 'border border-zinc-950/15 group-data-hover:border-zinc-950/30'
const checked = [
'group-data-checked:bg-(--checked-border) group-data-checked:before:bg-(--checked-background)',
'group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent',
]
const dark = [
'dark:bg-white/5 dark:border-white/15 dark:group-data-hover:border-white/30',
'dark:before:hidden dark:after:hidden dark:after:-inset-px dark:after:rounded-[0.3125rem]',
]
const darkChecked = [
'dark:group-data-checked:bg-(--checked-background) dark:group-data-checked:after:block',
'dark:group-data-checked:border-white/5 dark:group-data-hover:group-data-checked:border-white/5',
]
const focus = 'group-data-focus:outline-accent-500 group-data-focus:outline-2 group-data-focus:outline-offset-2'
const disabled = [
'group-data-disabled:opacity-50 group-data-disabled:bg-zinc-950/5 group-data-disabled:border-zinc-950/25',
'group-data-disabled:before:bg-transparent group-data-disabled:[--mark-color:var(--color-zinc-950)]/50',
'dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:border-white/20',
'dark:group-data-disabled:[--mark-color:var(--color-white)]/50 dark:group-data-checked:group-data-disabled:after:hidden',
]
const forcedColors = [
'forced-colors:[--checked-background:Highlight] dark:forced-colors:[--checked-background:Highlight]',
'forced-colors:[--mark-color:HighlightText] dark:forced-colors:[--mark-color:HighlightText]',
'forced-colors:group-data-disabled:[--mark-color:Highlight] dark:forced-colors:group-data-disabled:[--mark-color:Highlight]',
]
const box = [layout, background, highlight, border, checked, dark, darkChecked, focus, disabled, forcedColors, themeColors]
type CheckboxProps = {
color?: Color
className?: string
} & Omit<Headless.CheckboxProps, 'as' | 'className'>
export function Checkbox({ color = 'accent', className, ...props }: CheckboxProps) {
return (
<Headless.Checkbox data-slot="control" {...props} className={clsx(className, 'group inline-flex focus:outline-hidden')}>
<span style={cssVariables(colors[color])} className={clsx(box)}>
<CheckIcon className="size-4 sm:size-3.5 text-(--mark-color) opacity-0 group-data-checked:opacity-100 group-data-indeterminate:hidden" />
<MinusIcon className="hidden size-4 sm:size-3.5 text-(--mark-color) opacity-0 group-data-checked:opacity-100 group-data-indeterminate:block" />
</span>
</Headless.Checkbox>
)
}<script setup lang="ts">
import { CheckIcon, MinusIcon } from '@heroicons/vue/16/solid'
import { CheckboxRoot } from 'reka-ui'
import { computed, useAttrs, type CSSProperties } from 'vue'
import { useDisabled, useFieldControl } from './fields'
import { vInteractions } from './interactions'
defineOptions({ inheritAttrs: false })
type CheckboxColors = {
mark: string
background: string
border: string
dark?: Partial<Omit<CheckboxColors, 'dark'>>
}
const withOpacity = (color: string, percent: number) => `color-mix(in oklab, ${color} ${percent}%, transparent)`
const palette = (hue: string) => (shade: number) => `var(--color-${hue}-${shade})`
const accent = (shade: number) => `var(--accent-${shade},var(--color-blue-${shade}))`
const zinc = palette('zinc')
const white = 'var(--color-white)'
function whiteMark(color: (shade: number) => string, background: number, border: number, opacity = 90): CheckboxColors {
return { mark: white, background: color(background), border: withOpacity(color(border), opacity) }
}
function darkMark(color: (shade: number) => string, background: number, border: number): CheckboxColors {
return { mark: color(950), background: color(background), border: withOpacity(color(border), 80) }
}
const darkCheckbox = whiteMark(zinc, 900, 950)
const whiteCheckbox = { mark: zinc(900), background: white, border: withOpacity(zinc(950), 15) }
const colors = {
'dark/zinc': { ...darkCheckbox, dark: { background: zinc(600) } },
'dark/white': { ...darkCheckbox, dark: whiteCheckbox },
white: whiteCheckbox,
dark: darkCheckbox,
zinc: whiteMark(zinc, 600, 700),
red: whiteMark(palette('red'), 600, 700),
orange: whiteMark(palette('orange'), 500, 600),
amber: darkMark(palette('amber'), 400, 500),
yellow: darkMark(palette('yellow'), 300, 400),
lime: darkMark(palette('lime'), 300, 400),
green: whiteMark(palette('green'), 600, 700),
emerald: whiteMark(palette('emerald'), 600, 700),
teal: whiteMark(palette('teal'), 600, 700),
cyan: darkMark(palette('cyan'), 300, 400),
sky: whiteMark(palette('sky'), 500, 600, 80),
blue: whiteMark(palette('blue'), 600, 700),
accent: whiteMark(accent, 600, 700),
indigo: whiteMark(palette('indigo'), 500, 600),
violet: whiteMark(palette('violet'), 500, 600),
purple: whiteMark(palette('purple'), 500, 600),
fuchsia: whiteMark(palette('fuchsia'), 500, 600),
pink: whiteMark(palette('pink'), 500, 600),
rose: whiteMark(palette('rose'), 500, 600),
} satisfies Record<string, CheckboxColors>
type Color = keyof typeof colors
function cssVariables(colors: CheckboxColors) {
return {
'--checkbox-mark': colors.mark,
'--checkbox-fill': colors.background,
'--checkbox-border': colors.border,
'--checkbox-dark-mark': colors.dark?.mark,
'--checkbox-dark-fill': colors.dark?.background,
'--checkbox-dark-border': colors.dark?.border,
} as CSSProperties
}
const themeColors = [
'[--mark-color:var(--checkbox-mark)] [--checked-background:var(--checkbox-fill)] [--checked-border:var(--checkbox-border)]',
'dark:[--mark-color:var(--checkbox-dark-mark,var(--checkbox-mark))]',
'dark:[--checked-background:var(--checkbox-dark-fill,var(--checkbox-fill))]',
'dark:[--checked-border:var(--checkbox-dark-border,var(--checkbox-border))]',
]
const layout = 'relative isolate flex items-center justify-center size-4.5 sm:size-4 rounded-[0.3125rem]'
const background = [
'before:absolute before:-z-10 before:inset-0 before:rounded-[calc(0.3125rem-1px)]',
'before:shadow-sm before:bg-white',
]
const highlight = [
'after:absolute after:inset-0 after:rounded-[calc(0.3125rem-1px)]',
'after:shadow-[inset_0_1px_--theme(--color-white/15%)]',
]
const border = 'border border-zinc-950/15 group-data-hover:border-zinc-950/30'
const checked = [
'group-data-checked:bg-(--checked-border) group-data-checked:before:bg-(--checked-background)',
'group-data-checked:border-transparent group-data-hover:group-data-checked:border-transparent',
]
const dark = [
'dark:bg-white/5 dark:border-white/15 dark:group-data-hover:border-white/30',
'dark:before:hidden dark:after:hidden dark:after:-inset-px dark:after:rounded-[0.3125rem]',
]
const darkChecked = [
'dark:group-data-checked:bg-(--checked-background) dark:group-data-checked:after:block',
'dark:group-data-checked:border-white/5 dark:group-data-hover:group-data-checked:border-white/5',
]
const focus = 'group-data-focus:outline-accent-500 group-data-focus:outline-2 group-data-focus:outline-offset-2'
const disabled = [
'group-data-disabled:opacity-50 group-data-disabled:bg-zinc-950/5 group-data-disabled:border-zinc-950/25',
'group-data-disabled:before:bg-transparent group-data-disabled:[--mark-color:var(--color-zinc-950)]/50',
'dark:group-data-disabled:bg-white/2.5 dark:group-data-disabled:border-white/20',
'dark:group-data-disabled:[--mark-color:var(--color-white)]/50 dark:group-data-checked:group-data-disabled:after:hidden',
]
const forcedColors = [
'forced-colors:[--checked-background:Highlight] dark:forced-colors:[--checked-background:Highlight]',
'forced-colors:[--mark-color:HighlightText] dark:forced-colors:[--mark-color:HighlightText]',
'forced-colors:group-data-disabled:[--mark-color:Highlight] dark:forced-colors:group-data-disabled:[--mark-color:Highlight]',
]
const box = [layout, background, highlight, border, checked, dark, darkChecked, focus, disabled, forcedColors, themeColors]
const props = withDefaults(
defineProps<{
color?: Color
disabled?: boolean
indeterminate?: boolean
}>(),
{ color: 'accent' }
)
const model = defineModel<boolean>({ default: false })
const attrs = useAttrs()
const fieldControl = useFieldControl(attrs)
const isDisabled = useDisabled(() => props.disabled)
const innerAttrs = computed(() => {
const { class: _class, ...rest } = attrs
return rest
})
</script>
<template>
<CheckboxRoot
v-bind="innerAttrs"
v-interactions
data-slot="control"
:model-value="indeterminate ? 'indeterminate' : model"
:disabled="isDisabled"
:id="fieldControl.id.value"
:aria-describedby="fieldControl.describedBy.value"
:data-checked="model ? '' : undefined"
:data-indeterminate="indeterminate ? '' : undefined"
:data-disabled="isDisabled ? '' : undefined"
:class="[attrs.class, 'group inline-flex focus:outline-hidden']"
@update:model-value="model = !model"
>
<span :style="cssVariables(colors[color])" :class="box">
<CheckIcon class="size-4 sm:size-3.5 text-(--mark-color) opacity-0 group-data-checked:opacity-100 group-data-indeterminate:hidden" />
<MinusIcon class="hidden size-4 sm:size-3.5 text-(--mark-color) opacity-0 group-data-checked:opacity-100 group-data-indeterminate:block" />
</span>
</CheckboxRoot>
</template>Component API
| Prop | Default | Description |
|---|---|---|
Checkboxbuilt on the Headless UI <Headless.Checkbox> component | ||
checked | false | Ticked state when you control it yourself. |
onChange | - | Receives the new boolean each time the box is toggled. |
color | accent | Fill color of the ticked box. |
indeterminate | false | Draws a dash instead of a tick for a partial selection. |
disabled | false | Blocks interaction and dims the box. |
CheckboxFieldbuilt on the Headless UI <Headless.Field> component | ||
disabled | false | Disables the box and dims its label and description. |
CheckboxGrouprenders a <div> element | ||
| This component does not expose any component-specific props. | ||
| Prop | Default | Description |
|---|---|---|
Checkboxbuilt on the Reka UI <CheckboxRoot> component | ||
v-model | false | Two-way binding of the ticked state. |
color | accent | Fill color of the ticked box. |
indeterminate | false | Draws a dash instead of a tick for a partial selection. |
disabled | false | Blocks interaction and dims the box. |
CheckboxFieldrenders a <div> element | ||
disabled | false | Disables the box and dims its label and description. |
CheckboxGrouprenders a <div> element | ||
| This component does not expose any component-specific props. | ||
Usage
A checkbox records a choice that the user confirms later, usually with a save or submit button. Each box stands on its own, so any number of them can be ticked. When the change should apply the moment it is flipped, a switch reads better; when only one answer is allowed, use radio buttons.
Label a single option
CheckboxField places the box in front of its Label and links the two, so a click on the text toggles the box too. Write the label as the thing that happens when the box is ticked:
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Label } from '@/components/fieldset'
export default function Example() {
return (
<CheckboxField>
<Checkbox name="payout_receipts" />
<Label>Email me a receipt for each payout</Label>
</CheckboxField>
)
}<script setup lang="ts">
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import Label from '@/components/label.vue'
</script>
<template>
<CheckboxField>
<Checkbox name="payout_receipts" />
<Label>Email me a receipt for each payout</Label>
</CheckboxField>
</template>Explain the effect of an option
A Description goes under the label when the name alone does not say what changes. The label turns medium weight so the two lines stay easy to tell apart:
Adds the customer VAT or GST number under the billing address.
<CheckboxField>
<Checkbox name="tax_ids" />
<Label>Show tax IDs on invoices</Label>
<Description>Adds the customer VAT or GST number under the billing address.</Description>
</CheckboxField><CheckboxField>
<Checkbox name="tax_ids" />
<Label>Show tax IDs on invoices</Label>
<Description>Adds the customer VAT or GST number under the billing address.</Description>
</CheckboxField>Group related options
Wrap the fields in a CheckboxGroup for consistent spacing, and in a Fieldset with a Legend when the list needs a heading. Giving every box the same name sends the ticked values together:
import { Checkbox, CheckboxField, CheckboxGroup } from '@/components/checkbox'
import { Fieldset, Label, Legend } from '@/components/fieldset'
import { Text } from '@/components/text'
export default function Example() {
return (
<Fieldset>
<Legend>Webhook events</Legend>
<Text>Pick the events sent to https://api.northwind.io/hooks.</Text>
<CheckboxGroup>
<CheckboxField>
<Checkbox name="events" value="invoice.paid" defaultChecked />
<Label>invoice.paid</Label>
</CheckboxField>
<CheckboxField>
<Checkbox name="events" value="invoice.payment_failed" defaultChecked />
<Label>invoice.payment_failed</Label>
</CheckboxField>
<CheckboxField>
<Checkbox name="events" value="customer.created" />
<Label>customer.created</Label>
</CheckboxField>
</CheckboxGroup>
</Fieldset>
)
}<script setup lang="ts">
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import CheckboxGroup from '@/components/checkbox-group.vue'
import Fieldset from '@/components/fieldset.vue'
import Label from '@/components/label.vue'
import Legend from '@/components/legend.vue'
import Text from '@/components/text.vue'
import { ref } from 'vue'
const paid = ref(true)
const failed = ref(true)
const created = ref(false)
</script>
<template>
<Fieldset>
<Legend>Webhook events</Legend>
<Text>Pick the events sent to https://api.northwind.io/hooks.</Text>
<CheckboxGroup>
<CheckboxField>
<Checkbox name="events" value="invoice.paid" v-model="paid" />
<Label>invoice.paid</Label>
</CheckboxField>
<CheckboxField>
<Checkbox name="events" value="invoice.payment_failed" v-model="failed" />
<Label>invoice.payment_failed</Label>
</CheckboxField>
<CheckboxField>
<Checkbox name="events" value="customer.created" v-model="created" />
<Label>customer.created</Label>
</CheckboxField>
</CheckboxGroup>
</Fieldset>
</template>States
Start with the box ticked
Set defaultChecked when the recommended choice is on and you do not need to track the value in state:
Start the v-model ref at true to render the box ticked on first paint:
<CheckboxField>
<Checkbox name="auto_advance" defaultChecked />
<Label>Finalize drafts one hour after creation</Label>
</CheckboxField><script setup lang="ts">
import { ref } from 'vue'
const autoAdvance = ref(true)
</script>
<template>
<CheckboxField>
<Checkbox name="auto_advance" v-model="autoAdvance" />
<Label>Finalize drafts one hour after creation</Label>
</CheckboxField>
</template>Reflect a partial selection
A parent box that drives several children should show a dash while only some of them are ticked. Compute indeterminate from the children and let the parent tick or clear them all:
'use client'
import { useState } from 'react'
import { Checkbox, CheckboxField, CheckboxGroup } from '@/components/checkbox'
import { Label } from '@/components/fieldset'
let columns = ['Invoice number', 'Customer', 'Amount']
export default function Example() {
let [selected, setSelected] = useState(['Invoice number'])
return (
<CheckboxGroup role="group" aria-label="Export columns">
<CheckboxField>
<Checkbox
checked={selected.length > 0}
indeterminate={selected.length > 0 && selected.length !== columns.length}
onChange={(checked) => setSelected(checked ? [...columns] : [])}
/>
<Label>All columns</Label>
</CheckboxField>
{columns.map((column) => (
<CheckboxField key={column}>
<Checkbox
name="columns"
value={column}
checked={selected.includes(column)}
onChange={(checked) =>
setSelected((current) =>
checked ? [...current, column] : current.filter((item) => item !== column)
)
}
/>
<Label>{column}</Label>
</CheckboxField>
))}
</CheckboxGroup>
)
}<script setup lang="ts">
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import CheckboxGroup from '@/components/checkbox-group.vue'
import Label from '@/components/label.vue'
import { computed, ref } from 'vue'
const columns = ['Invoice number', 'Customer', 'Amount']
const selected = ref(['Invoice number'])
const all = computed({
get: () => selected.value.length > 0,
set: (checked: boolean) => {
selected.value = checked ? [...columns] : []
},
})
const partial = computed(() => selected.value.length > 0 && selected.value.length !== columns.length)
function toggle(column: string, checked: boolean) {
selected.value = checked ? [...selected.value, column] : selected.value.filter((item) => item !== column)
}
</script>
<template>
<CheckboxGroup role="group" aria-label="Export columns">
<CheckboxField>
<Checkbox v-model="all" :indeterminate="partial" />
<Label>All columns</Label>
</CheckboxField>
<CheckboxField v-for="column in columns" :key="column">
<Checkbox
name="columns"
:value="column"
:model-value="selected.includes(column)"
@update:model-value="toggle(column, $event)"
/>
<Label>{{ column }}</Label>
</CheckboxField>
</CheckboxGroup>
</template>Show an option the plan does not include
Keeping an unavailable option on screen tells people it exists. disabled on the CheckboxField blocks the box and dims its label and description, and the description is a good place to say how to unlock it:
Download invoices and payouts as a spreadsheet.
Available on the Scale plan.
<CheckboxGroup>
<CheckboxField>
<Checkbox name="exports" value="csv" defaultChecked />
<Label>CSV export</Label>
<Description>Download invoices and payouts as a spreadsheet.</Description>
</CheckboxField>
<CheckboxField disabled>
<Checkbox name="exports" value="audit_log" />
<Label>Audit log export</Label>
<Description>Available on the Scale plan.</Description>
</CheckboxField>
</CheckboxGroup><CheckboxGroup>
<CheckboxField>
<Checkbox name="exports" value="csv" v-model="csv" />
<Label>CSV export</Label>
<Description>Download invoices and payouts as a spreadsheet.</Description>
</CheckboxField>
<CheckboxField disabled>
<Checkbox name="exports" value="audit_log" />
<Label>Audit log export</Label>
<Description>Available on the Scale plan.</Description>
</CheckboxField>
</CheckboxGroup>To disable a lone box outside a field, put disabled on the Checkbox itself.
Forms
Keep the value in your own state
Pass checked and onChange when other parts of the screen depend on the box, such as a summary or a save button:
Bind a ref with v-model when other parts of the screen depend on the box, such as a summary or a save button:
Receipts: on
'use client'
import { useState } from 'react'
import { Checkbox, CheckboxField } from '@/components/checkbox'
import { Label } from '@/components/fieldset'
export default function Example() {
let [receipts, setReceipts] = useState(true)
return (
<>
<CheckboxField>
<Checkbox checked={receipts} onChange={setReceipts} />
<Label>Email me a receipt for each payout</Label>
</CheckboxField>
<p>Receipts: {receipts ? 'on' : 'off'}</p>
</>
)
}<script setup lang="ts">
import Checkbox from '@/components/checkbox.vue'
import CheckboxField from '@/components/checkbox-field.vue'
import Label from '@/components/label.vue'
import { ref } from 'vue'
const receipts = ref(true)
</script>
<template>
<CheckboxField>
<Checkbox v-model="receipts" />
<Label>Email me a receipt for each payout</Label>
</CheckboxField>
<p>Receipts: {{ receipts ? 'on' : 'off' }}</p>
</template>Choose what the form submits
With a name, the box takes part in native form submission. value sets what is sent for it, which defaults to on:
<CheckboxField>
<Checkbox name="events" value="payout.paid" />
<Label>payout.paid</Label>
</CheckboxField><CheckboxField>
<Checkbox name="events" value="payout.paid" />
<Label>payout.paid</Label>
</CheckboxField>Customization
Change the fill color
The ticked box follows your theme accent, and falls back to blue when none is set. Pass color when a box should carry a meaning of its own, like green for a settled payment:
<CheckboxField>
<Checkbox color="emerald" checked={checked} onChange={setChecked} />
<Label>Mark INV-2041 as paid</Label>
</CheckboxField><CheckboxField>
<Checkbox color="emerald" v-model="checked" />
<Label>Mark INV-2041 as paid</Label>
</CheckboxField>Available colors
dark/zinc and dark/white swap shades between light and dark mode. The other 20 names keep the same fill in both:
<Checkbox color="dark/zinc" checked={checked} onChange={setChecked} />
<Checkbox color="dark/white" checked={checked} onChange={setChecked} />
<Checkbox color="teal" checked={checked} onChange={setChecked} /><Checkbox color="dark/zinc" v-model="checked" />
<Checkbox color="dark/white" v-model="checked" />
<Checkbox color="teal" v-model="checked" />Place the box after its label
When the field layout does not fit, for example a filter row with the label first, render the Checkbox alone and point your own <label> at it with id and htmlFor:
When the field layout does not fit, for example a filter row with the label first, render the Checkbox alone and point your own <label> at it with id and for:
<div className="flex items-center justify-between gap-4">
<label htmlFor="overdue" className="text-base/6 text-zinc-950 select-none sm:text-sm/6 dark:text-white">
Only overdue invoices
</label>
<Checkbox id="overdue" name="overdue" />
</div><div class="flex items-center justify-between gap-4">
<label for="overdue" class="text-base/6 text-zinc-950 select-none sm:text-sm/6 dark:text-white">
Only overdue invoices
</label>
<Checkbox id="overdue" name="overdue" />
</div>Accessibility
Inside a CheckboxField, the Label names the box and the Description is announced with it. Outside a field, nothing names the box for you.
Name a box that has no visible label
Row selection in a table is the usual case. Add an aria-label that says which record the box belongs to:
<Checkbox name="selected" value="INV-2041" aria-label="Select INV-2041" />