Checkbox
A control that enables users to make binary (or ternary) choices through selection and deselection.
import { component$, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
export default component$(() => {
useStyles$(styles);
return (
<Checkbox.Root>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
);
});
import { LuCheck } from "@qwikest/icons/lucide";
// example styles
import styles from "./checkbox.css?inline";
Features
- WAI ARIA Checkbox design pattern
- Indeterminate state support
- Form binding with hidden native input
- Error message handling and validation
- Custom description text with screenreader support
- Reactive state management with signals
- Keyboard navigation with Enter key handling
- Accessible labeling system
- Disabled state management
- Two-way data binding with bind:checked prop
- Custom onChange event handling
- Automatic ARIA state management
- Visual indicator customization
- Compound component architecture
Anatomy
Part | Description |
---|---|
<Checkbox.Root> | Root component that provides context and state management for the checkbox |
<Checkbox.Indicator> | Visual indicator component showing the checkbox state |
<Checkbox.Trigger> | Interactive trigger component that handles checkbox toggling |
<Checkbox.Label> | Label component for the checkbox |
<Checkbox.Description> | A component that renders the description text for a checkbox |
<Checkbox.HiddenInput> | A hidden native checkbox input for form submission |
<Checkbox.ErrorMessage> | A component that displays error messages for a checkbox |
Adding a label
To associate a label with a checkbox, use the Checkbox.Label
component.
import { component$, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
return (
<Checkbox.Root class="checkbox-root">
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>Checkbox Label</Checkbox.Label>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Adding a description
To add a description to a checkbox, add the isDescription
prop in the Checkbox.Root
component.
Then use the Checkbox.Description
component to decide where to render the description.
import { component$, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const description =
"By checking this box, you acknowledge that you have read, understood, and agree to our Terms of Service and Privacy Policy. This includes consent to process your personal data as described in our policies.";
return (
<Checkbox.Root isDescription>
<div
style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}
>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
<Checkbox.Description>
By checking this box, you acknowledge that you have read, understood, and agree to
our Terms of Service and Privacy Policy. This includes consent to process your
personal data as described in our policies.
</Checkbox.Description>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Note: Due to HTML streaming limitations, the
isDescription
prop must be passed to the root component to maintain consistent accessibility across different environments (especially server rendered content). The component will display a warning if no description is provided when this component is rendered.
Initially check a checkbox
To set a checkbox to its initial checked state, use the checked
prop.
import { component$, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
return (
<Checkbox.Root checked>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Reactive state
To make a checkbox reactive, use the bind:checked
prop, and pass in a signal.
Checked: false
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isChecked = useSignal(false);
return (
<>
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<p>Checked: {isChecked.value ? "true" : "false"}</p>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
Programmatic state
To make programmatic changes to a checkbox, change a signal value passed to the bind:checked
prop.
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isChecked = useSignal(false);
return (
<>
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<button
type="button"
onClick$={() => {
isChecked.value = true;
}}
>
I check the checkbox above
</button>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
onChange$
To listen for changes when the checkbox state changes, use the onChange$
prop.
Times changed: 0
import { $, component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const numChanges = useSignal(0);
const isChecked = useSignal(false);
const handleChange$ = $((checked: boolean) => {
numChanges.value++;
isChecked.value = checked;
});
return (
<>
<Checkbox.Root onChange$={handleChange$}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<p>Times changed: {numChanges.value}</p>
<section>New value: {isChecked.value ? "true" : "false"}</section>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
Disabled
To disable a checkbox, use the disabled
prop.
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isDisabled = useSignal(true);
return (
<>
<Checkbox.Root disabled={isDisabled.value}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<button
type="button"
onClick$={() => {
isDisabled.value = !isDisabled.value;
}}
>
Toggle disabled state
</button>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
Mixed state
Checkboxes can also be in a third mixed or indeterminate state. This is often considered a "partially checked" state.
To set a mixed state, pass "mixed"
to the bind:checked
prop:
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck, LuMinus } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isChecked = useSignal<boolean | "mixed">("mixed");
return (
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
{isChecked.value === "mixed" ? <LuMinus /> : <LuCheck />}
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
The mixed state can also be set reactively
import { component$, useSignal, useStyles$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck, LuMinus } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const isChecked = useSignal<boolean | "mixed">(false);
return (
<>
<Checkbox.Root bind:checked={isChecked}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
{isChecked.value === "mixed" ? <LuMinus /> : <LuCheck />}
</Checkbox.Indicator>
</Checkbox.Trigger>
</Checkbox.Root>
<button
type="button"
onClick$={() => {
isChecked.value = "mixed";
}}
>
Make the checkbox mixed
</button>
</>
);
});
// example styles
import styles from "./checkbox.css?inline";
When a checkbox is in a mixed state and the user clicks it:
- The first click changes the state from "mixed" to "checked"
- The next click changes the state from "checked" to "unchecked"
This follows the standard accessibility pattern where mixed → checked → unchecked → checked → unchecked, and so on.
Inside a form
To create a form with a checkbox, use the Checkbox.HiddenNativeInput
component.
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const formData = useSignal<Record<string, FormDataEntryValue>>();
return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});
export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms">
<Checkbox.HiddenInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Then create a name for the checkbox in the Checkbox.Root
component.
This will be used to associate the checkbox with the form.
Making a checkbox required
To make a checkbox required, pass required
to the Checkbox.Root
component.
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const formData = useSignal<Record<string, FormDataEntryValue>>();
return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});
export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms" required>
<Checkbox.HiddenInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Giving a checkbox a value
By default, the value of a checkbox is on
when checked. To give a checkbox a distinct value, pass a string to the value
prop.
import { $, component$, useSignal, useStyles$, useStylesScoped$ } from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const formData = useSignal<Record<string, FormDataEntryValue>>();
return (
<form
preventdefault:submit
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});
export const TermsCheckbox = component$(() => {
return (
<Checkbox.Root name="terms" value="checked">
<Checkbox.HiddenInput />
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
Checkbox validation
To validate a checkbox, use the Checkbox.ErrorMessage
component. Whenever this component is rendered, the checkbox will display a validation error message.
Screen readers will announce the error message when the component is rendered, along with an indication that the checkbox is invalid.
import {
type Signal,
component$,
useComputed$,
useSignal,
useStyles$
} from "@builder.io/qwik";
import { Checkbox } from "@kunai-consulting/qwik";
import { LuCheck } from "@qwikest/icons/lucide";
export default component$(() => {
useStyles$(styles);
const formData = useSignal<Record<string, FormDataEntryValue>>();
const isChecked = useSignal(false);
const isSubmitAttempt = useSignal(false);
const isError = useComputed$(() => !isChecked.value && isSubmitAttempt.value);
return (
<form
preventdefault:submit
noValidate
onSubmit$={(e) => {
const form = e.target as HTMLFormElement;
if (!isChecked.value) {
isSubmitAttempt.value = true;
return;
}
formData.value = Object.fromEntries(new FormData(form));
}}
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
>
<TermsCheckbox isChecked={isChecked} isError={isError} />
<button type="submit">Submit</button>
{formData.value && <div>Submitted: {JSON.stringify(formData.value, null, 2)}</div>}
</form>
);
});
type TermsCheckboxProps = {
isChecked: Signal<boolean>;
isError: Signal<boolean>;
};
export const TermsCheckbox = component$(({ isChecked, isError }: TermsCheckboxProps) => {
return (
<Checkbox.Root name="terms" required bind:checked={isChecked}>
<Checkbox.HiddenInput />
<div
style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }}
>
<Checkbox.Trigger class="checkbox-trigger">
<Checkbox.Indicator class="checkbox-indicator">
<LuCheck />
</Checkbox.Indicator>
</Checkbox.Trigger>
<Checkbox.Label>I accept the Terms and Conditions</Checkbox.Label>
</div>
{isError.value && (
<Checkbox.ErrorMessage style={{ color: "red" }}>
Please accept the terms and conditions
</Checkbox.ErrorMessage>
)}
</Checkbox.Root>
);
});
// example styles
import styles from "./checkbox.css?inline";
API Reference
Checkbox Root
Inherits from: <div />
Props
Prop | Type | Default | Description |
---|---|---|---|
"bind:checked" | Signal<boolean | "mixed"> | - | |
checked | T | - | Initial checked state of the checkbox |
onChange$ | QRL<(checked: T) => void> | - | Event handler called when the checkbox state changes |
disabled | boolean | - | Whether the checkbox is disabled |
isDescription | boolean | - | Whether the checkbox has a description |
name | string | - | Name attribute for the hidden input element |
required | boolean | - | Whether the checkbox is required |
value | string | - | Value attribute for the hidden input element |
Data Attributes
Attribute | Description |
---|---|
data-disabled | Indicates whether the checkbox is disabled |
data-checked | Indicates whether the checkbox is checked |
data-mixed | Indicates whether the checkbox is in an indeterminate state |
Checkbox Indicator
Inherits from: <span />
Data Attributes
Attribute | Description |
---|---|
data-hidden | Indicates whether the indicator should be hidden based on checkbox state |
data-checked | Indicates whether the checkbox is in a checked state |
data-mixed | Indicates whether the checkbox is in an indeterminate state |
Checkbox Trigger
Inherits from: <button />
Data Attributes
Attribute | Description |
---|---|
data-disabled | Indicates whether the checkbox trigger is disabled |
data-checked | Indicates whether the checkbox trigger is checked |
data-mixed | Indicates whether the checkbox trigger is in an indeterminate state |
Checkbox Description
Inherits from: <div />
Checkbox Error Message
Inherits from: <div />
Checkbox Hidden Input
Inherits from: <input />
Checkbox Label
Inherits from: <label />
Accessibility
Keyboard Interactions
Key | Description |
---|---|
Space | When focus is on the checkbox trigger, toggles the checkbox state between checked and unchecked |
Enter | When focus is on the checkbox trigger, toggles the checkbox state between checked and unchecked (default behavior prevented) |
Tab | Moves focus to the checkbox trigger or away from it following the document tab sequence |