Skip to content
Ndop UI

Checkbox

A control that's checked, unchecked or mixed, for choosing an option on its own or several from a list.

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 checkbox

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.

Email me about
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.

Read them before you sign up.

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
SpaceChecks or unchecks it; a mixed checkbox becomes checked.
EnterDoes 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 checkbox

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.

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.
booleanfalse
[disabled]
Disables it. A disabled bound field disables it too.
booleanfalse
[required]
Marks it required: aria-required="true".
booleanfalse
[invalid]
Marks it invalid when no forms library is bound; with one, the field's state decides.
booleanfalse
[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)].
booleanfalse
[(indeterminate)]
Whether it's mixed: aria-checked="mixed" whatever checked is. Activating it clears this. Two-way bindable.
booleanfalse
Data attribute Values
data-slotcheckbox
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-slotcheckbox-indicator
data-state —

Agent notes

Do

  • Name every checkbox with a label, even inside a table row.
  • Use indeterminate for 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.