Checkbox
An accessible checkbox — sizes, indeterminate and invalid states, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Checkbox toggles a single value on or off, with support for an
indeterminate ("some selected") state. Built on Radix — controlled (checked
onCheckedChange) or uncontrolled (defaultChecked). In Angular it's<bpdm-checkbox>, driven by[(checked)],[(ngModel)], or reactive forms.
Usage
Pair the checkbox with a <label> linked by id so clicking the text toggles it.
import { Checkbox } from '@bpdm/ui/checkbox';
export function Terms() {
return (
<div className="flex items-center gap-2.5">
<Checkbox id="terms" defaultChecked />
<label htmlFor="terms">Accept terms & conditions</label>
</div>
);
}import { Component } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'terms',
imports: [BpdmCheckbox],
templateUrl: './checkbox-usage.html',
})
export class Terms {}<label class="flex items-center gap-2.5">
<bpdm-checkbox [checked]="true" /> Accept terms & conditions
</label>States
Unchecked, checked, indeterminate, and disabled (on or off). Set
checked="indeterminate" (React) / indeterminate (Angular) for the mixed state.
import { Checkbox } from '@bpdm/ui/checkbox';
export function States() {
return (
<div className="flex items-center gap-4">
<Checkbox aria-label="Unchecked" />
<Checkbox defaultChecked aria-label="Checked" />
<Checkbox checked="indeterminate" aria-label="Indeterminate" />
<Checkbox disabled aria-label="Disabled" />
<Checkbox disabled defaultChecked aria-label="Disabled and checked" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'app-states',
standalone: true,
imports: [BpdmCheckbox],
template: `
<div class="flex items-center gap-4">
<bpdm-checkbox aria-label="Unchecked" />
<bpdm-checkbox [checked]="true" aria-label="Checked" />
<bpdm-checkbox indeterminate aria-label="Indeterminate" />
<bpdm-checkbox disabled aria-label="Disabled" />
<bpdm-checkbox disabled [checked]="true" aria-label="Disabled and checked" />
</div>
`,
})
export class StatesComponent {}Sizes
The size prop offers sm, md (default), and lg.
import { Checkbox } from '@bpdm/ui/checkbox';
export function Sizes() {
return (
<div className="flex items-center gap-4">
<Checkbox size="sm" defaultChecked aria-label="Small" />
<Checkbox size="md" defaultChecked aria-label="Medium" />
<Checkbox size="lg" defaultChecked aria-label="Large" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'app-sizes',
standalone: true,
imports: [BpdmCheckbox],
template: `
<div class="flex items-center gap-4">
<bpdm-checkbox size="sm" [checked]="true" aria-label="Small" />
<bpdm-checkbox size="md" [checked]="true" aria-label="Medium" />
<bpdm-checkbox size="lg" [checked]="true" aria-label="Large" />
</div>
`,
})
export class SizesComponent {}Invalid
Set aria-invalid for the error state (turns destructive-coloured) and link an
error message with aria-describedby so screen readers announce it.
You must accept before continuing.
import { Checkbox } from '@bpdm/ui/checkbox';
export function Invalid() {
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2.5">
<Checkbox id="agree" aria-invalid aria-describedby="agree-err" />
<label htmlFor="agree">I agree to the terms</label>
</div>
<p id="agree-err" className="text-xs text-destructive">
You must accept before continuing.
</p>
</div>
);
}import { Component } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'app-invalid',
standalone: true,
imports: [BpdmCheckbox],
template: `
<div class="flex flex-col gap-1.5">
<label class="flex items-center gap-2.5">
<bpdm-checkbox aria-invalid="true" aria-describedby="agree-err" /> I agree to the terms
</label>
<p id="agree-err" class="text-xs text-destructive">You must accept before continuing.</p>
</div>
`,
})
export class InvalidComponent {}Checkbox group
A set of independent options — each is its own checkbox with a linked label.
import { Checkbox } from '@bpdm/ui/checkbox';
const prefs = [
{ id: 'g-email', label: 'Email notifications', checked: true },
{ id: 'g-sms', label: 'SMS notifications', checked: false },
{ id: 'g-push', label: 'Push notifications', checked: true },
];
export function Preferences() {
return (
<div className="flex flex-col gap-3">
{prefs.map((o) => (
<div key={o.id} className="flex items-center gap-2.5">
<Checkbox id={o.id} defaultChecked={o.checked} />
<label htmlFor={o.id}>{o.label}</label>
</div>
))}
</div>
);
}import { Component } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'app-preferences',
standalone: true,
imports: [BpdmCheckbox],
template: `
@for (o of prefs; track o.label) {
<label class="flex items-center gap-2.5">
<bpdm-checkbox [(checked)]="o.on" /> {{ o.label }}
</label>
}
`,
})
export class PreferencesComponent {
prefs = [
{ label: 'Email notifications', on: true },
{ label: 'SMS notifications', on: false },
{ label: 'Push notifications', on: true },
];
}Select all (indeterminate)
A parent checkbox reflects its children: checked when all are on, indeterminate when only some are, unchecked when none. Toggling it sets them all.
import { useState } from 'react';
import { Checkbox } from '@bpdm/ui/checkbox';
const toppings = ['Cheese', 'Mushrooms', 'Olives'];
export function SelectAll() {
const [checked, setChecked] = useState([true, false, false]);
const all = checked.every(Boolean);
const some = checked.some(Boolean);
const parent = all ? true : some ? 'indeterminate' : false;
return (
<div className="flex flex-col gap-3">
<label className="flex items-center gap-2.5 font-medium">
<Checkbox
checked={parent}
onCheckedChange={(v) => setChecked(toppings.map(() => v === true))}
/>
Select all
</label>
{toppings.map((t, i) => (
<label key={t} className="flex items-center gap-2.5 ps-1">
<Checkbox
checked={checked[i]}
onCheckedChange={(v) =>
setChecked((prev) => prev.map((c, j) => (j === i ? v === true : c)))
}
/>
{t}
</label>
))}
</div>
);
}indeterminate is a separate input from [(checked)], so compute it from the
children in your component:
import { Component, computed, signal } from '@angular/core';
import { BpdmCheckbox } from '@bpdm/ng';
@Component({
selector: 'app-select-all',
standalone: true,
imports: [BpdmCheckbox],
template: `
<label class="flex items-center gap-2.5 font-medium">
<bpdm-checkbox
[checked]="allChecked()"
[indeterminate]="someChecked() && !allChecked()"
(checkedChange)="toggleAll($event)" />
Select all
</label>
@for (t of toppings(); track t.name) {
<label class="flex items-center gap-2.5 ps-1">
<bpdm-checkbox [checked]="t.on" (checkedChange)="set(t.name, $event)" /> {{ t.name }}
</label>
}
`,
})
export class SelectAllComponent {
readonly toppings = signal([
{ name: 'Cheese', on: true },
{ name: 'Mushrooms', on: false },
{ name: 'Olives', on: false },
]);
readonly allChecked = computed(() => this.toppings().every((t) => t.on));
readonly someChecked = computed(() => this.toppings().some((t) => t.on));
set(name: string, on: boolean): void {
this.toppings.update((list) => list.map((t) => (t.name === name ? { ...t, on } : t)));
}
toggleAll(on: boolean): void {
this.toppings.update((list) => list.map((t) => ({ ...t, on })));
}
}API
| Prop | Type | Default | Description |
|---|---|---|---|
checked / defaultChecked | boolean | 'indeterminate' | — | Controlled / uncontrolled state. 'indeterminate' shows the mixed glyph. |
onCheckedChange | (checked: boolean | 'indeterminate') => void | — | React — fires on toggle. Angular uses [(checked)] / [(ngModel)]. |
indeterminate | boolean | false | Angular — mixed state (React uses checked="indeterminate"). |
size | sm | md | lg | md | Control size. |
disabled | boolean | false | Disable the control. |
aria-invalid | boolean | — | Error state. |
id | string | — | Control id (label association / testing), forwarded to the checkbox. |
aria-label / aria-labelledby / aria-describedby | string | — | Accessible name / description, forwarded to the checkbox. |
In React, all native Radix Checkbox props (name, value, required, id,
aria-*, data-*, …) are spread onto the control and the ref is forwarded. In
Angular, size / indeterminate / aria-invalid / id / aria-label /
aria-labelledby / aria-describedby are @Inputs on <bpdm-checkbox>, which is
also a ControlValueAccessor ([(ngModel)] / reactive forms) with a standalone
[(checked)].
Accessibility
- A true
role="checkbox"witharia-checked(true/false/mixed) and keyboard support (Space toggles; Tab moves focus with a visible ring). - Give it an accessible name — link a
<label>byid(htmlForin React,forin Angular), wrap the control in the label, or passaria-label/aria-labelledby. These reach the control in both frameworks. - Set
aria-invalidwhen validation fails and link the message witharia-describedbyso assistive tech reads the error. - The indeterminate state is communicated as
aria-checked="mixed", not by the glyph alone — and the check/dash glyph isaria-hidden, so nothing is announced twice.
Internationalization
- The checkbox renders no text of its own, so there's nothing to translate inside
it — pass a translated accessible name (
aria-label, or the<label>you associate) and translated help/error text viaaria-describedby. - It's RTL-safe by construction: the control is a symmetric box with no physical
spacing, so it mirrors correctly under
dir="rtl"(the box/label order flips with your layout's writing direction). See the Internationalization guide.