Checkbox
A control that's checked, unchecked or mixed, for choosing an option on its own or several from a list.
- v1.0.0
- WAI-ARIA: Checkbox
- SSR
- Zoneless
- 2.4 kB gzip
You can enable or disable notifications at any time.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Checkbox, CheckboxIndicator } from '@/ui/checkbox/checkbox';
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from '@/ui/form-field/form-field';
import { Label } from '@/ui/label/label';
/** A checkbox with its label, and one with a description beside it. */
@Component({
selector: 'example-checkbox-demo',
imports: [
Checkbox,
CheckboxIndicator,
Field,
FieldContent,
FieldDescription,
FieldLabel,
Label,
NdIcon,
],
template: `
<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<button ndCheckbox id="checkbox-demo-terms">
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<label ndLabel for="checkbox-demo-terms">
Accept terms and conditions
</label>
</div>
<div ndField orientation="horizontal">
<button ndCheckbox id="checkbox-demo-notify" [defaultChecked]="true">
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<div ndFieldContent>
<label ndFieldLabel for="checkbox-demo-notify">
Enable notifications
</label>
<p ndFieldDescription>
You can enable or disable notifications at any time.
</p>
</div>
</div>
</div>
`,
})
export class CheckboxDemoExample {}
A checkbox turns one option on or off, on its own, such as accepting the
terms, or as one of several in a list. It's a native button announced as a
checkbox, so it works with a keyboard, screen readers and any forms
library: Signal Forms, reactive or template-driven. Set indeterminate
for a "select all" box when only some items are selected; activating it
checks it. A label whose for is its id names it and toggles it too.
Installation
npx @ndop/cli add checkboxpnpm dlx @ndop/cli add checkboxyarn dlx @ndop/cli add checkboxbunx @ndop/cli add checkboxInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/checkbox/checkbox.tsimport { computed, Directive, input } from '@angular/core'; import { NdCheckbox, NdCheckboxIndicator } from '@ndop/primitives/checkbox'; import { cn } from '@/lib/utils'; /** * A checkbox on a native `button` (`@ndop/primitives/checkbox`): checked, * unchecked or, with `indeterminate`, mixed. Bind it with `[(checked)]` or * any forms library, name it with a `label[ndLabel]` whose `for` is its * `id`, and put a `ndCheckboxIndicator` with a check icon inside. * * @example * ```html * <button ndCheckbox id="terms" [(checked)]="accepted"> * <span ndCheckboxIndicator><svg ndIcon="check"></svg></span> * </button> * <label ndLabel for="terms">Accept terms and conditions</label> * ``` */ @Directive({ selector: 'button[ndCheckbox]', hostDirectives: [ { directive: NdCheckbox, inputs: [ 'checked', 'indeterminate', 'defaultChecked', 'disabled', 'required', 'invalid', 'id', ], outputs: ['checkedChange', 'indeterminateChange'], }, ], host: { 'data-slot': 'checkbox', '[class]': 'classes()' }, }) export class Checkbox { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary', 'data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:data-[state=indeterminate]:bg-primary', this.class(), ), ); } /** * The checkmark of a {@link Checkbox}, shown while it's checked or mixed. * Put a check icon inside, or a minus icon for a mixed checkbox. * * @example * ```html * <span ndCheckboxIndicator><svg ndIcon="check"></svg></span> * ``` */ @Directive({ selector: '[ndCheckboxIndicator]', hostDirectives: [NdCheckboxIndicator], host: { 'data-slot': 'checkbox-indicator', '[class]': 'classes()' }, }) export class CheckboxIndicator { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'grid place-content-center text-current transition-none [&>svg]:size-3.5', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Checkbox, CheckboxIndicator } from '@/ui/checkbox/checkbox';
import {
Field,
FieldContent,
FieldDescription,
FieldLabel,
} from '@/ui/form-field/form-field';
import { Label } from '@/ui/label/label';<div class="flex flex-col gap-6">
<div class="flex items-center gap-3">
<button ndCheckbox id="checkbox-demo-terms">
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<label ndLabel for="checkbox-demo-terms">
Accept terms and conditions
</label>
</div>
<div ndField orientation="horizontal">
<button ndCheckbox id="checkbox-demo-notify" [defaultChecked]="true">
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<div ndFieldContent>
<label ndFieldLabel for="checkbox-demo-notify">
Enable notifications
</label>
<p ndFieldDescription>
You can enable or disable notifications at any time.
</p>
</div>
</div>
</div>Examples
Checkbox disabled
A disabled checkbox, checked and unchecked, with dimmed labels.
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Checkbox, CheckboxIndicator } from '@/ui/checkbox/checkbox';
import { Label } from '@/ui/label/label';
/** A disabled checkbox, checked and unchecked, with dimmed labels. */
@Component({
selector: 'example-checkbox-disabled',
imports: [Checkbox, CheckboxIndicator, Label, NdIcon],
template: `
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<button ndCheckbox id="checkbox-disabled-off" disabled>
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<label ndLabel for="checkbox-disabled-off">Sync contacts</label>
</div>
<div class="flex items-center gap-3">
<button
ndCheckbox
id="checkbox-disabled-on"
disabled
[defaultChecked]="true"
>
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<label ndLabel for="checkbox-disabled-on">Required by your plan</label>
</div>
</div>
`,
})
export class CheckboxDisabledExample {}
Checkbox indeterminate
A select-all checkbox that's mixed while only some of the list is checked.
import { Component, computed, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import { Checkbox, CheckboxIndicator } from '@/ui/checkbox/checkbox';
import { Label } from '@/ui/label/label';
/**
* A select-all checkbox: checked when every item is, mixed when some are,
* and checking or clearing them all when activated.
*/
@Component({
selector: 'example-checkbox-indeterminate',
imports: [Checkbox, CheckboxIndicator, Label, NdIcon],
template: `
<fieldset class="flex flex-col gap-3">
<legend class="sr-only">Email me about</legend>
<div class="flex items-center gap-3">
<button
ndCheckbox
id="checkbox-indeterminate-all"
[checked]="allChecked()"
[indeterminate]="someChecked()"
(checkedChange)="setAll($event)"
>
<span ndCheckboxIndicator>
<svg [ndIcon]="someChecked() ? 'minus' : 'check'"></svg>
</span>
</button>
<label ndLabel for="checkbox-indeterminate-all">All updates</label>
</div>
@for (topic of topics(); track topic.id) {
<div class="flex items-center gap-3 ps-7">
<button
ndCheckbox
[id]="'checkbox-indeterminate-' + topic.id"
[checked]="topic.checked"
(checkedChange)="set(topic.id, $event)"
>
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<label ndLabel [for]="'checkbox-indeterminate-' + topic.id">
{{ topic.label }}
</label>
</div>
}
</fieldset>
`,
})
export class CheckboxIndeterminateExample {
protected readonly topics = signal([
{ id: 'product', label: 'Product news', checked: true },
{ id: 'events', label: 'Events near me', checked: false },
{ id: 'tips', label: 'Tips and tutorials', checked: false },
]);
protected readonly allChecked = computed(() =>
this.topics().every((topic) => topic.checked),
);
protected readonly someChecked = computed(
() => !this.allChecked() && this.topics().some((topic) => topic.checked),
);
/**
* Checks or clears one topic.
*
* @param id - The topic.
* @param checked - Its new state.
*/
protected set(id: string, checked: boolean): void {
this.topics.update((topics) =>
topics.map((topic) => (topic.id === id ? { ...topic, checked } : topic)),
);
}
/**
* Checks or clears every topic.
*
* @param checked - The new state.
*/
protected setAll(checked: boolean): void {
this.topics.update((topics) =>
topics.map((topic) => ({ ...topic, checked })),
);
}
}
Checkbox form
A required terms checkbox in a Signal Forms form, with its error once touched.
import { Component, signal } from '@angular/core';
import { form, FormField, required } from '@angular/forms/signals';
import { NdIcon } from '@ndop/core';
import { Button } from '@/ui/button/button';
import { Checkbox, CheckboxIndicator } from '@/ui/checkbox/checkbox';
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
} from '@/ui/form-field/form-field';
/**
* A terms checkbox that must be checked to continue, in a Signal Forms
* form: its error shows once the box has been left unchecked, or the form
* was submitted.
*/
@Component({
selector: 'example-checkbox-form',
imports: [
Button,
Checkbox,
CheckboxIndicator,
Field,
FieldContent,
FieldDescription,
FieldError,
FieldLabel,
FormField,
NdIcon,
],
template: `
<form class="flex max-w-sm flex-col gap-6" (submit)="submit($event)">
<div ndField orientation="horizontal">
<button ndCheckbox id="checkbox-form-terms" [formField]="signup.terms">
<span ndCheckboxIndicator><svg ndIcon="check"></svg></span>
</button>
<div ndFieldContent>
<label ndFieldLabel for="checkbox-form-terms">
I agree to the terms of service
</label>
<p ndFieldDescription>Read them before you sign up.</p>
<p ndFieldError>Agree to the terms to create your account.</p>
</div>
</div>
<button ndButton type="submit" class="self-start">Create account</button>
</form>
`,
})
export class CheckboxFormExample {
protected readonly model = signal({ terms: false });
protected readonly signup = form(this.model, (path) => {
required(path.terms);
});
/**
* Marks the fields touched, so errors show, instead of reloading.
*
* @param event - The form's submit event.
*/
protected submit(event: Event): void {
event.preventDefault();
this.signup().markAsTouched();
}
}
Accessibility
Implements the Checkbox pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Space | Checks or unchecks it; a mixed checkbox becomes checked. |
| Enter | Does nothing, as for a native checkbox (APG); the default is prevented. |
Focus
- Unchanged from a native button: activating it keeps focus on it.
- Leaving it marks a bound field touched.
Right-to-left
Nothing mirrors.
API reference
Checkbox
button[ndCheckbox]role checkboxA checkbox on a native button (@ndop/primitives/checkbox): checked,
unchecked or, with indeterminate, mixed. Bind it with [(checked)] or
any forms library, name it with a label[ndLabel] whose for is its
id, and put a ndCheckboxIndicator with a check icon inside.
Manages aria-checked, aria-required, aria-invalid.
| Checkbox binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
[defaultChecked]Initial checked when it isn't bound. | boolean | false |
[disabled]Disables it. A disabled bound field disables it too. | boolean | false |
[required]Marks it required: aria-required="true". | boolean | false |
[invalid]Marks it invalid when no forms library is bound; with one, the field's
state decides. | boolean | false |
[id]Element id; also the key a label[ndLabel] uses to find it. | string | undefined | — |
[(checked)]Whether it's checked. Two-way bindable with [(checked)]. | boolean | false |
[(indeterminate)]Whether it's mixed: aria-checked="mixed" whatever checked is.
Activating it clears this. Two-way bindable. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | checkbox |
data-state | — |
data-disabled | — |
data-invalid | — |
CheckboxIndicator
[ndCheckboxIndicator]The checkmark of a Checkbox, shown while it's checked or mixed.
Put a check icon inside, or a minus icon for a mixed checkbox.
| CheckboxIndicator binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | checkbox-indicator |
data-state | — |
Agent notes
Do
- Name every checkbox with a
label, even inside a table row. - Use
indeterminatefor a parent box whose children are partly checked. - Put related checkboxes in a fieldset with a legend.
Don't
- Don't use a checkbox for a setting that applies right away; use a Switch.
- Don't use checkboxes for options that exclude each other; use a Radio Group.