Qure UI
Components

Checkbox

Selects any number of options from a set, with an indeterminate state for partial selection.

import { Checkbox } from '@/registry/qure/ui/checkbox'export default function CheckboxDemo() {  return (    <label className="flex items-center gap-2 text-sm">      <Checkbox /> Include prior studies    </label>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/checkbox.json

Usage

import { Checkbox } from '@/components/ui/checkbox'
<label className="flex items-center gap-2 text-sm">
  <Checkbox defaultChecked onCheckedChange={setOn} /> Include prior studies
</label>

A checkbox is for any number of choices from none upwards, and it takes effect when the surrounding form is submitted. If the change applies the instant it is made — a display setting, a notification preference — use a Switch, which says so by looking like a control rather than a form field. One choice from several is a Radio Group.

Wrap the box and its text in a <label>. The words are a far larger target than an 18px square, and the association is what lets a screen reader announce the two together.

Sizes

import { Checkbox } from '@/registry/qure/ui/checkbox'export default function CheckboxSize() {  return (    <div className="flex flex-col gap-3">      <label className="flex items-center gap-2 text-xs">        <Checkbox size="sm" defaultChecked /> Small — a dense worklist row      </label>      <label className="flex items-center gap-2 text-sm">        <Checkbox size="md" defaultChecked /> Medium — the default, and a form      </label>      <label className="flex items-center gap-2 text-base">        <Checkbox size="lg" defaultChecked /> Large — a touch target on a reporting console      </label>    </div>  )}

16, 18 and 20px, with the tick scaled to match. md is the Figma box and the default. Use sm in a dense table row where the checkbox sits on a single line of 12px text; lg where a finger rather than a pointer is doing the work.

Every size keeps the same invisible hit area — the component draws after:-inset-x-3 after:-inset-y-2 around itself, so a small box is still comfortably clickable without the row growing.

State

import { Checkbox } from '@/registry/qure/ui/checkbox'export default function CheckboxState() {  return (    <>      <Checkbox />      <Checkbox defaultChecked />      <Checkbox indeterminate />      <Checkbox disabled />      <Checkbox defaultChecked disabled />    </>  )}

Unchecked, checked, indeterminate, disabled, and checked-and-disabled. A disabled checked box still shows its tick: a locked control has to keep reporting what it is locked to.

Select all

1 of 4 series selected.

'use client'import * as React from 'react'import { Checkbox } from '@/registry/qure/ui/checkbox'const series = [  { id: 'ax-soft', label: 'Axial soft tissue — 402 images' },  { id: 'ax-lung', label: 'Axial lung — 402 images' },  { id: 'cor', label: 'Coronal reformat — 210 images' },  { id: 'sag', label: 'Sagittal reformat — 190 images' },]export default function CheckboxTree() {  const [selected, setSelected] = React.useState<string[]>(['ax-soft'])  const all = selected.length === series.length  const some = selected.length > 0 && !all  function toggle(id: string, checked: boolean) {    setSelected(current => (checked ? [...current, id] : current.filter(s => s !== id)))  }  return (    <div className="flex w-full max-w-sm flex-col gap-3">      <label className="flex items-center gap-2 text-sm font-medium">        <Checkbox          checked={all}          indeterminate={some}          // A click on a partly-selected parent means "select all", never          // "return to the dash" — there is nothing a third state could mean.          onCheckedChange={checked => setSelected(checked ? series.map(s => s.id) : [])}        />        Send series to the archive      </label>      <div className="border-l pl-4">        {series.map(({ id, label }) => (          <label key={id} className="flex items-center gap-2 py-1 text-sm">            <Checkbox              checked={selected.includes(id)}              onCheckedChange={checked => toggle(id, checked)}            />            {label}          </label>        ))}      </div>      <p className="text-muted-foreground text-xs">        {selected.length} of {series.length} series selected.      </p>    </div>  )}

The indeterminate state means "some of the children below are selected". The parent is indeterminate because of the children — it is a state you put the box into from code, never one a person can click their way back to. Click an indeterminate box and it becomes checked; click again and it clears.

<Checkbox
  checked={selected.length === all.length}
  indeterminate={selected.length > 0 && selected.length < all.length}
  onCheckedChange={checked => setSelected(checked ? all : [])}
/>

Base UI renders one indicator for both checked and indeterminate and tells you which through state.indeterminate, which is why there is a single element here rather than a tick and a dash that could disagree.

A "select all" that only selects the page in front of you, in a worklist that is paginated, is a well-known way to delete the wrong studies. Say which set it covers — "select all 12 on this page" — or make the count next to it do the saying.

With labels and descriptions

Pulls the two most recent studies of the same modality for comparison.

De-identified before it leaves the department.

Unavailable until the patient consent form is on file.

'use client'import { Checkbox } from '@/registry/qure/ui/checkbox'import { Field, FieldDescription, FieldItem, FieldLabel } from '@/registry/qure/ui/field'export default function FieldCheckbox() {  return (    <Field className="max-w-sm">      <FieldItem>        <FieldLabel>          <Checkbox defaultChecked name="priors" />          Retrieve priors        </FieldLabel>        <FieldDescription>          Pulls the two most recent studies of the same modality for comparison.        </FieldDescription>      </FieldItem>      <FieldItem>        <FieldLabel>          <Checkbox name="teaching" />          Add to the teaching file        </FieldLabel>        <FieldDescription>De-identified before it leaves the department.</FieldDescription>      </FieldItem>      <FieldItem disabled>        <FieldLabel>          <Checkbox name="research" />          Share with the research cohort        </FieldLabel>        <FieldDescription>Unavailable until the patient consent form is on file.</FieldDescription>      </FieldItem>    </Field>  )}

Field supplies the label association and the per-option description. FieldItem lays the row out with a 16px gap and indents the description past the box, so it lines up with the label rather than the control. disabled on the item greys the label and the description alongside the box, which keeps the reason for the disabling readable.

A filter panel

Filters
Modality
Status
'use client'import * as React from 'react'import { Badge } from '@/registry/qure/ui/badge'import { Button } from '@/registry/qure/ui/button'import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/registry/qure/ui/card'import { Checkbox } from '@/registry/qure/ui/checkbox'import { Separator } from '@/registry/qure/ui/separator'const groups = [  { name: 'Modality', options: [['ct', 'CT', 42], ['mr', 'MR', 18], ['cr', 'X-ray', 96]] },  { name: 'Status', options: [['unread', 'Unread', 23], ['draft', 'Draft', 4], ['signed', 'Signed', 129]] },] as constexport default function CheckboxFilters() {  const [active, setActive] = React.useState<string[]>(['ct', 'unread'])  function toggle(id: string, checked: boolean) {    setActive(current => (checked ? [...current, id] : current.filter(a => a !== id)))  }  return (    <Card className="w-full max-w-xs">      <CardHeader>        <CardTitle>Filters</CardTitle>      </CardHeader>      <CardContent className="flex flex-col gap-4">        {groups.map(group => (          // A fieldset, so the group's name is announced with each option          // rather than each checkbox arriving unattached.          <fieldset key={group.name} className="flex flex-col gap-2">            <legend className="text-muted-foreground mb-1 text-xs font-medium">{group.name}</legend>            {group.options.map(([id, label, count]) => (              <label key={id} className="flex items-center gap-2 text-sm">                <Checkbox                  size="sm"                  checked={active.includes(id)}                  onCheckedChange={checked => toggle(id, checked)}                />                {label}                <span className="text-muted-foreground ml-auto text-xs tabular-nums">{count}</span>              </label>            ))}          </fieldset>        ))}      </CardContent>      <Separator />      <CardFooter className="justify-between">        <Badge>{active.length} active</Badge>        <Button size="sm" variant="tertiary" onClick={() => setActive([])} disabled={active.length === 0}>          Clear        </Button>      </CardFooter>    </Card>  )}

Grouped checkboxes need a group name, and a <fieldset> with a <legend> is the cheapest way to get one — otherwise "CT" and "Unread" arrive as six unattached options. Counts on the right tell the reader what a filter will cost them before they apply it.

Contrast

The resting outline is --border-base-tertiary at 1px: 3.63:1 light, 4.17:1 dark. WCAG 2.2 SC 1.4.11 asks 3:1 of a UI component boundary, and an unchecked box is nothing but its boundary.

The previous library hardcoded rgba(0,130,128,0.12) here — 12% teal, measuring 1.10:1, in practice invisible against a white surface. If you change a control's border, measure it rather than picking by eye.

Keyboard

Space toggles. Tab reaches each box individually — a set of checkboxes is not one tab stop the way a radio group is, because each is an independent decision. There is no arrow-key navigation and there should not be.

API Reference

Everything Base UI's Checkbox accepts, plus:

Prop

Type

The Base UI props worth knowing:

Prop

Type

parent also exists, for a select-all driven by Base UI's Checkbox Group rather than by your own state. We do not ship a CheckboxGroup wrapper, so the parent/child logic here is the handful of lines in the select-all example above.

On this page