---
title: Checkbox
description: A native checkbox with its words attached, painted by accent-color rather than redrawn.
source: https://adamjurek.com/components/checkbox
---

## Home


Checkbox is a native `<input type="checkbox">` with its label attached,
painted in the site's accent color by `accent-color` rather than hidden and
redrawn. Reach for it for any single yes/no or multi-select choice in a form.

<!-- ::start:showcase demo="checkbox" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

One CSS property, `accent-color`, is the entire style layer. That keeps every
native behaviour a hand-drawn checkbox has to rebuild - keyboard handling,
form submission, the indeterminate state, screen-reader semantics - for the
cost of a single declaration, and a custom-drawn replacement earns none of it
back.

## What it does not do

It does not render a custom checkmark or animate the check. The box is the
browser's own, so its look is whatever `accent-color` and the platform agree
on, not a shape pulled pixel for pixel from a mockup.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/checkbox.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/checkbox.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Forms |
| Files | `checkbox.tsx` |
| Dependencies | None |
| Tags | form, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { useState } from "react";
import { Checkbox } from "@sushindustries/ui";

export function Example() {
	const [checked, setChecked] = useState(false);

	return (
		<Checkbox
			label="Email me about releases"
			checked={checked}
			onChange={(event) => setChecked(event.target.checked)}
		/>
	);
}
```

## What you should see

A native checkbox painted in the site's accent colour when checked, with
the label text beside it - clicking the label toggles the box too, since
both are wrapped in one `<label>`. There is no custom check mark drawn by
this component; the tick is the browser's own.

## If nothing happens

A checkbox that will not check itself and never calls `onChange` usually
means it was rendered with `checked` but no `onChange` - React treats
that as a read-only control and blocks input, which is correct behaviour
for a controlled component missing its other half, not a bug in
`Checkbox`.


## Guides


## It is a native input, not a redrawn one

Every prop except `type` and `label` passes straight through to a real
`<input type="checkbox">` - `checked`, `onChange`, `disabled`,
`aria-describedby`, all of it. `accent-color` simply recolours the
platform's own control, so keyboard handling, form submission,
`indeterminate` and screen-reader behaviour are the browser's for free,
and there is nothing this component can get wrong that a plain `<input>`
could not.

## Grouping several

For a list of checkboxes, `.choice-group` in the stylesheet gives the
stacked spacing they are meant to sit inside - `Checkbox` itself has no
opinion about its siblings and renders one row regardless of how many
others surround it.


## API


<!-- generated:api -->

## Props

Accepts every prop of `Omit<InputHTMLAttributes<HTMLInputElement>, "type">`, plus:

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `label` | `string` | - |  |

<!-- /generated:api -->

## Notes

Every prop except `type` passes straight through to the underlying
`<input>`, so `checked`, `onChange`, `disabled`, `required` and any
`aria-*` attribute all work exactly as they would on a plain checkbox.
`type` is not exposed at all - the component fixes it to `"checkbox"`,
so there is no way to accidentally render a radio through this
component.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="checkbox" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { useState } from "react";
import { Button, Checkbox } from "@sushindustries/ui";

export function TermsForm() {
	const [agreed, setAgreed] = useState(false);

	return (
		<form className="flex col gap-4">
			<Checkbox
				label="I agree to the terms"
				checked={agreed}
				onChange={(event) => setAgreed(event.target.checked)}
			/>
			<Button type="submit" disabled={!agreed}>
				Continue
			</Button>
		</form>
	);
}
```

## What this example is not

The checkbox here is controlled - `checked` and `onChange` are both set,
which is what lets `Button`'s `disabled` state react to it. An
uncontrolled checkbox (no `checked` prop, just `defaultChecked`) works
fine on its own, but nothing else on the page can read its value without
also reaching for a ref.
