Skip to main content

Checkbox

A form control that allows the user to toggle between checked and not checked. Supports indeterminate state.

Pair with Field.* for label/description/error wiring in forms:

You must accept before continuing.

import { Checkbox } from "@ngrok/mantle/checkbox";import { Field } from "@ngrok/mantle/field"; <Field.Item name="terms-of-service">	<Field.Label className="flex items-center gap-2">		<Field.Control>			<Checkbox />		</Field.Control>		Accept terms and conditions	</Field.Label>	<Field.Description>You must accept before continuing.</Field.Description></Field.Item>;

Or render the control on its own:

import { Checkbox } from "@ngrok/mantle/checkbox";import { Label } from "@ngrok/mantle/label"; <Label htmlFor="terms" className="flex items-center gap-2">	<Checkbox name="terms" id="terms" />	Accept terms and conditions</Label><Label htmlFor="unchecked" className="flex items-center gap-2">	<Checkbox id="unchecked" name="unchecked" checked={false} readOnly />	Unchecked</Label><Label htmlFor="checked" className="flex items-center gap-2">	<Checkbox id="checked" name="checked" checked readOnly />	Checked</Label><Label htmlFor="indeterminate" className="flex items-center gap-2">	<Checkbox id="indeterminate" name="indeterminate" defaultChecked="indeterminate" readOnly />	Indeterminate</Label>

Multi-line label with description

For a checkbox with an emphasized title and a supplementary description beneath it, compose it inside Choice: drop the Checkbox into Choice.Indicator and pair Choice.Label (a real <label>, so clicking it toggles the box) with an optional Choice.Description. The description forwards its click to that label, so the whole text block toggles the box. The box aligns to the leading of the title's first line, and the description associates via aria-describedby.

Flags this domain as a disposable or throwaway email provider.

import { Checkbox } from "@ngrok/mantle/checkbox";import { Choice } from "@ngrok/mantle/choice"; <Choice.Root>	<Choice.Indicator>		<Checkbox defaultChecked />	</Choice.Indicator>	<Choice.Content>		<Choice.Label>Mark as disposable email provider</Choice.Label>		<Choice.Description>			Flags this domain as a disposable or throwaway email provider.		</Choice.Description>	</Choice.Content></Choice.Root>;

Wrap that same Choice in a Field.Item / Field.Control to bind a form name and pick up validation. The field's id / name / aria-* flow onto the checkbox, and the Field.Description (or a Field.Errors) is merged into the control's aria-describedby alongside the Choice.Description (no duplicate ids, no manual wiring):

Flags this domain as a disposable or throwaway email provider.

Applied the next time this domain is scanned.

import { Checkbox } from "@ngrok/mantle/checkbox";import { Choice } from "@ngrok/mantle/choice";import { Field } from "@ngrok/mantle/field"; <Field.Item name="disposable">	<Field.Control>		<Choice.Root>			<Choice.Indicator>				<Checkbox defaultChecked />			</Choice.Indicator>			<Choice.Content>				<Choice.Label>Mark as disposable email provider</Choice.Label>				<Choice.Description>					Flags this domain as a disposable or throwaway email provider.				</Choice.Description>			</Choice.Content>		</Choice.Root>	</Field.Control>	<Field.Description>Applied the next time this domain is scanned.</Field.Description></Field.Item>;

For the fully-clickable list-row variant (and the full API), see the Choice docs.

With TanStack Form

Use @tanstack/react-form with zod to keep the checkbox controlled and render errors through Field.Errors.

import { Button } from "@ngrok/mantle/button";import { Checkbox } from "@ngrok/mantle/checkbox";import { Field, toErrorMessages } from "@ngrok/mantle/field";import { useForm } from "@tanstack/react-form";import { z } from "zod"; export const formSchema = z.object({	acceptedTerms: z.boolean().refine((value) => value, "Accept the terms to continue."),});function Example() {	const defaultValues = {		acceptedTerms: false,	};	const form = useForm({		defaultValues,		validators: {			onSubmit: formSchema,		},		onSubmit: ({ value }) => {			// Handle form submission here		},	}); 	return (		<form			className="space-y-4"			onSubmit={(event) => {				event.preventDefault();				event.stopPropagation();				void form.handleSubmit();			}}		>			<form.Field name="acceptedTerms">				{(field) => (					<Field.Item name={field.name}>						<Field.Label className="flex items-center gap-2">							<Field.Control>								<Checkbox									checked={field.state.value}									onBlur={field.handleBlur}									onChange={(event) => field.handleChange(event.target.checked)}								/>							</Field.Control>							Accept terms and conditions						</Field.Label>						<Field.Errors messages={toErrorMessages(field.state.meta.errors)} />					</Field.Item>				)}			</form.Field>			<Button type="submit" appearance="filled" intent="neutral">				Submit			</Button>		</form>	);}

Select-all helper

selectAllChecked resolves the tri-state checked value for a "select all" checkbox from the current selection counts: true when everything is selected, "indeterminate" when only some is, and false when nothing is. Use it to keep the header checkbox of a multi-select list correct without hand-rolling the tri-state branch. It pairs directly with a DataTable row-selection column:

import { Checkbox, selectAllChecked } from "@ngrok/mantle/checkbox";import { Label } from "@ngrok/mantle/label";import { useState } from "react"; function Example() {	const rows = ["Row 1", "Row 2", "Row 3"];	const [selected, setSelected] = useState<string[]>([]);	const allSelected = selected.length === rows.length;	const someSelected = selected.length > 0 && !allSelected; 	return (		<div className="flex flex-col gap-2">			<Label className="flex items-center gap-2">				<Checkbox					aria-label="Select all rows"					checked={selectAllChecked({ allSelected, someSelected })}					onChange={(event) => setSelected(event.target.checked ? [...rows] : [])}				/>				Select all			</Label>			{rows.map((row) => (				<Label key={row} className="flex items-center gap-2 pl-6">					<Checkbox						checked={selected.includes(row)}						onChange={(event) =>							setSelected((prev) =>								event.target.checked ? [...prev, row] : prev.filter((value) => value !== row),							)						}					/>					{row}				</Label>			))}		</div>	);}

Returns boolean | "indeterminate" (a CheckedState), ready to pass straight to Checkbox's checked prop. allSelected takes precedence, so it wins even if a caller also reports someSelected.

OptionTypeDescription
allSelectedbooleanWhether every selectable item is currently selected.
someSelectedbooleanWhether some (but not necessarily all) items are selected.

API Reference

Checkbox

All props from input[type="checkbox"], plus:

PropTypeDefaultDescription
checked?boolean | "indeterminate"Whether the checkbox is checked or not. Setting this to "indeterminate" will show the indeterminate state. This is useful for parent-child relationships, but requires manual, controlled state.
defaultChecked?boolean | "indeterminate"The checked state of the checkbox when it is initially rendered. Use when you do not need to control its checked state.
readOnly?booleanfalseBlocks the toggle and sets aria-readonly. The native readonly attribute is inert on a checkbox, so mantle guards the click itself.
validation?"error" | "success" | "warning" | false | () => "error" | "success" | "warning" | falseUse the validation prop to show a specific validation status. This will change the border and outline of the checkbox. The false type is useful with short-circuiting logic. Setting validation to "error" also sets aria-invalid.

For a checkbox with a titled, multi-line label and a description, see Choice.