DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Checkbox

A form control for selecting one or more options: checked, unchecked, and indeterminate states, five sizes and intents, rich labels, select-all groups, cards, and full-row mobile layouts.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Checkbox, CheckboxGroup } from "@tessinaui/ui";
{/* Standalone */}
<Checkbox label="Accept terms and conditions" />

{/* Controlled */}
<Checkbox
  checked={accepted}
  onCheckedChange={(val) => setAccepted(val as boolean)}
  label="Accept terms"
/>

{/* With description and error */}
<Checkbox
  label="Accept terms"
  required
  errorMessage="You must accept the terms to continue."
/>

{/* Mobile settings row: text leading, control trailing, whole row tappable */}
<Checkbox label="Deals and offers" labelPlacement="start" fullWidth />

When to use

  • Checkbox — pick any number of options from a set, or a single yes/no that is submitted with a form (terms consent).
  • Radio — exactly one of a set.
  • Switch — a standalone setting with an immediate effect, no form submit.
  • Chip (filter) — multi-select directly over the content it filters.

Write labels as short positive fragments in sentence case ("Send me updates", never "Don't send me emails"), no trailing punctuation. Labels wrap — they are never truncated.

Examples

Default

A basic checkbox with a label. The label, the box, and the space between them are one tap target.

Sizes

Five sizes from xs to xl; box, mark, and label scale together.

Intents

Semantic colors — none, error, warning, success, and info. The unchecked warning border and all helper text step to accessible token shades (see Accessibility).

States

Unchecked, checked, indeterminate, disabled, read-only, and loading. Read-only stays at full opacity and keeps keyboard focus but refuses interaction; loading shows a spinner in the box and reports aria-busy.

Description and error

Helper text below the label, or a validation message with an alert icon. The message is announced politely and colored by intent — with no explicit intent it defaults to error styling, so intent="warning" + errorMessage is a warning message, not an error repainted red.

Terms with links

label accepts ReactNode: links inside the label navigate without toggling the checkbox.

Group

A CheckboxGroup renders a single labeled <fieldset>; value/onValueChange manage the checked set by each child's name.

Select all (parent)

Pass parent to one checkbox inside a controlled group with allValues — Base UI derives its checked/indeterminate state and the mixed → all → none → last-partial cycle. Do not wire checked/indeterminate on the parent yourself.

Mobile rows

fullWidth + labelPlacement="start" is the settings-row shape: text leading, control trailing, the entire row one tap target.

Contact list

Checkbox composed with Avatar for selecting people.

Cards

CheckboxCard turns the entire surface into a selectable option with a title, description, and corner indicator.

API Reference

Checkbox

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Box 14/16/20/24/28px; label and mark scale with it
intent"none" | "error" | "warning" | "success" | "info""none"Semantic color for border, fill, and message
rounded"none" | "sm" | "md" | "lg" | "full""md"Box corner radius. full is the circular selection indicator — there is no separate shape prop
motion"draw" | "fade" | "none""draw"How the mark arrives. draw strokes the glyph along its own path; fade cross-fades it in at 150ms; none shows it instantly. All three flatten to an instant change under prefers-reduced-motion: reduce, so none is not the OS-level accessibility answer — reach for it for render cost on dense lists, or to wire your own in-app "reduce motion" setting
checked / defaultCheckedboolean— / falseControlled / uncontrolled state
onCheckedChange(checked: boolean, eventDetails) => void—Change handler
indeterminatebooleanfalseMixed state (aria-checked="mixed", dash mark)
disabledbooleanfalseNot focusable (aria-disabled, out of tab order)
readOnlybooleanfalseFocusable but not operable; full-opacity display
isLoadingbooleanfalseSpinner in the box, aria-busy, interaction refused
requiredbooleanfalseNative required + decorative asterisk
optionalbooleanfalseMuted "(optional)" marker; ignored when required
labelReactNode—Visible label; the wrapping <label> makes it the accessible name
visuallyHiddenLabelbooleanfalseKeep the name, hide the text (sr-only)
descriptionReactNode—Helper text below the label; reaches the control via aria-describedby, never the name
errorMessageReactNode—Validation message (replaces description), aria-live="polite", sets aria-invalid
labelPlacement"start" | "end""end"Logical text side (RTL-correct)
fullWidthbooleanfalseRow stretches; text column grows, box sits at the far edge
dir"ltr" | "rtl"—Text direction for the row
name / valuestring—Form submission; inside a group, name is the membership key
parentbooleanfalseSelect-all inside a controlled CheckboxGroup (needs allValues)
classNamestring—Extra classes on the wrapper <div>

Deprecated: labelPosition ("left" \| "right") — maps onto labelPlacement (left → start), removed next minor.

All other props (inputRef, uncheckedValue, nativeButton, form, render, ARIA/data attributes) are forwarded to the Base UI Checkbox.Root — a <span role="checkbox"> with a hidden <input> beside it.

CheckboxGroup

PropTypeDefaultDescription
value / defaultValuestring[]— / []Controlled / uncontrolled checked names
onValueChange(value: string[], eventDetails) => void—Change handler
allValuesstring[]—Every value in the group — required for a parent checkbox
disabledbooleanfalseDisables all children
orientation"vertical" | "horizontal""vertical"Stack direction
size / intent / rounded / motion—"md" / "none" / — / "draw"Propagated to children unless they set their own
labelReactNode—Legend of the rendered <fieldset>
description / errorMessageReactNode—Group helper / validation, both wired via aria-describedby
required / optionalbooleanfalseLegend markers only — put required on the controls themselves
dir"ltr" | "rtl"—Direction for the fieldset

CheckboxCard

PropTypeDefaultDescription
titlestringrequiredCard title = accessible name
descriptionReactNode—Exposed via aria-describedby, never the name
iconReactNode—Leading icon slot
sizeCheckboxSize"md"Indicator size
intentCheckboxIntent"none"Border/fill/text semantics
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""xl"Card corners; the corner indicator follows
motion"draw" | "fade" | "none""draw"How the mark arrives. draw strokes the glyph along its own path; fade cross-fades it in at 150ms; none shows it instantly. All three flatten to an instant change under prefers-reduced-motion: reduce, so none is not the OS-level accessibility answer — reach for it for render cost on dense lists, or to wire your own in-app "reduce motion" setting
dir"ltr" | "rtl"—Direction

Plus all Base UI root props (checked, value, disabled, …). CheckboxCardGroup adds columns (1–4), the same label/description/errorMessage fieldset chrome, and propagates size/intent/rounded/motion.

CheckboxSkeleton

size, rounded, labelPlacement, fullWidth, showLabel, showDescription, showError, labelChars, descriptionChars, dir — mirrors the real boxes exactly (the frame is the checkbox's own cva).

Do / Don't

  • Do use one Checkbox for a submitted yes/no (terms); don't use a Switch there — switches imply immediate effect.
  • Do give a parent checkbox a controlled group with allValues; don't hand-wire its checked/indeterminate.
  • Do keep labels positive ("Email me"); don't negate ("Don't email me") — checking a negative is a double negative.
  • Do use errorMessage on the group for "pick at least one"; don't mark every child invalid.
  • Do use rounded="full" for circular mobile selection indicators; don't invent a shape prop or confuse circles with radios in forms.

Accessibility

Pattern: WAI-ARIA APG Checkbox (tri-state for parent).

KeyAction
Tab / Shift+TabMove focus between checkboxes (each is a tab stop)
SpaceToggle the focused checkbox
EnterNothing — deliberately suppressed to match the APG (Base UI's default would toggle)
  • The row is a wrapping <label>: box, text, and gap are one click/tap target; nested links in a rich label navigate without toggling.
  • Accessible name = label content. The description is aria-hidden inside the label and reaches the control via aria-describedby (hidden referents resolve); the required asterisk is decorative — the control's own required/aria-required carries the semantic.
  • errorMessage renders in the text column as an aria-live="polite" region, sets aria-invalid, and joins aria-describedby; it cannot join the accessible name because the name comes from the label text span via aria-labelledby.
  • aria-checked="mixed" for indeterminate; a parent checkbox announces the mixed state and controls the set.
  • Groups render one <fieldset> captioned by its <legend> — a single role="group" announcement (the fieldset is the Base UI group element).
  • Disabled = aria-disabled, removed from the tab order. Read-only = focusable, aria-readonly, not operable (review-mode pattern). Loading = aria-busy + refused interaction.
  • Touch targets: 44×44px hit area per box on touch viewports with ≥44px row pitch (overlays shrink to the 24px WCAG 2.5.8 minimum at md+ so stacked rows never overlap); fullWidth makes the whole row the target.
  • Contrast (hard-gated in pnpm audit:contrast, light + dark): unchecked border --outline-border 3.03:1/3.01:1; warning unchecked border + helper text use warning-tinted-foreground (7.14:1 light); helper/error text mirrors Label's map; checked fills use the locked on-* glyph pairs; card selected ink 4.72–13.97:1.
  • Forced colors (Windows High Contrast): states map to SelectedItem/SelectedItemText/GrayText so checked and unchecked stay distinguishable.
  • Reduced motion: the mark's 150ms fade/zoom, press scale, and spinner rotation are all disabled or substituted under prefers-reduced-motion.
  • AT matrix: VoiceOver + NVDA passes pending (status stays beta until both are on record).
ChatBubbleNewChip

On this page

PlaygroundInstallationUsageWhen to useExamplesDefaultSizesIntentsStatesDescription and errorTerms with linksGroupSelect all (parent)Mobile rowsContact listCardsAPI ReferenceCheckboxCheckboxGroupCheckboxCardCheckboxSkeletonDo / Don'tAccessibility