Inputs
Checkbox
A native checkbox with a label and optional description, for yes or no choices and multi-select lists.
- Built on
<input type="checkbox">appearance: none:checked:indeterminate:user-invalid:has()- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 2.1 kB
- Dependencies
- React
Try its options
Loading its options.
Code
import { Checkbox } from "@/components/equip/checkbox";
export default function Demo() {
return (
<div style={{ display: "grid", gap: 4, width: 200 }}>
<Checkbox name="notify" value="deploys" label="Deploy finished" defaultChecked />
<Checkbox name="notify" value="failures" label="Build failed" defaultChecked />
<Checkbox name="notify" value="comments" label="New comment" />
</div>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:4px;width:200px">
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" class="eq-checkbox-input" name="notify" checked value="deploys"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label">Deploy finished</span>
</span>
</label>
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" class="eq-checkbox-input" name="notify" checked value="failures"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label">Build failed</span>
</span>
</label>
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" class="eq-checkbox-input" name="notify" value="comments"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label">New comment</span>
</span>
</label>
</div>Examples
With a description
Pass an id with a description so screen readers hear the label as the name and the description after it.
Code
import { Checkbox } from "@/components/equip/checkbox";
export default function Example() {
return (
<div style={{ display: "grid", gap: 12, maxWidth: 320 }}>
<Checkbox
id="checkbox-desc-2fa"
name="security"
value="2fa"
label="Require two-factor sign-in"
description="Members without it are asked to set it up at their next sign-in."
defaultChecked
/>
<Checkbox
id="checkbox-desc-sso"
name="security"
value="sso"
label="Allow SSO only"
description="Turns off email and password sign-in for everyone in the workspace."
/>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:12px;max-width:320px">
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" id="checkbox-desc-2fa" class="eq-checkbox-input" aria-labelledby="checkbox-desc-2fa-label" aria-describedby="checkbox-desc-2fa-description" name="security" checked value="2fa"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label" id="checkbox-desc-2fa-label">Require two-factor sign-in</span>
<span class="eq-checkbox-description" id="checkbox-desc-2fa-description">Members without it are asked to set it up at their next sign-in.</span>
</span>
</label>
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" id="checkbox-desc-sso" class="eq-checkbox-input" aria-labelledby="checkbox-desc-sso-label" aria-describedby="checkbox-desc-sso-description" name="security" value="sso"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label" id="checkbox-desc-sso-label">Allow SSO only</span>
<span class="eq-checkbox-description" id="checkbox-desc-sso-description">Turns off email and password sign-in for everyone in the workspace.</span>
</span>
</label>
</div>Indeterminate
Only script can set the native indeterminate property, so this example is a client component that sets it through a ref.
Code
"use client";
import { Checkbox } from "@/components/equip/checkbox";
export default function Example() {
return (
<div style={{ display: "grid", gap: 4, width: 240 }}>
<Checkbox
name="regions-all"
label="All regions"
ref={(el) => {
if (el) el.indeterminate = true;
}}
/>
<div style={{ display: "grid", gap: 4, paddingInlineStart: 24 }}>
<Checkbox name="regions" value="eu-west-1" label="eu-west-1" defaultChecked />
<Checkbox name="regions" value="us-east-1" label="us-east-1" defaultChecked />
<Checkbox name="regions" value="ap-south-1" label="ap-south-1" />
</div>
</div>
);
}Disabled
Code
import { Checkbox } from "@/components/equip/checkbox";
export default function Example() {
return (
<div style={{ display: "grid", gap: 4 }}>
<Checkbox name="plan" value="audit-log" label="Audit log (Enterprise plan)" disabled />
<Checkbox name="plan" value="backups" label="Daily backups (always on)" defaultChecked disabled />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:4px">
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input disabled type="checkbox" class="eq-checkbox-input" name="plan" value="audit-log"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label">Audit log (Enterprise plan)</span>
</span>
</label>
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input disabled type="checkbox" class="eq-checkbox-input" name="plan" checked value="backups"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label">Daily backups (always on)</span>
</span>
</label>
</div>With an error
With an id, the error is linked by aria-describedby and the input gets aria-invalid; the box edge turns red until it is ticked.
Code
import { Checkbox } from "@/components/equip/checkbox";
export default function Example() {
return (
<Checkbox
id="terms"
name="terms"
label="I agree to the terms of service"
error="Accept the terms to create your workspace."
/>
);
}HTMLworks in any framework, with the part's CSS
<label class="eq-checkbox">
<span class="eq-checkbox-box"><input type="checkbox" id="terms" class="eq-checkbox-input" aria-labelledby="terms-label" aria-describedby="terms-error" aria-invalid="true" name="terms"><svg class="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" /></svg><svg class="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M4.75 8h6.5" pathLength="1" /></svg></span>
<span class="eq-checkbox-text">
<span class="eq-checkbox-label" id="terms-label">I agree to the terms of service</span>
<span class="eq-checkbox-error" id="terms-error">Accept the terms to create your workspace.</span>
</span>
</label>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
label | ReactNode | none | Visible label. Clicking it toggles the box. |
description | ReactNode | none | Secondary line under the label. Pass id as well so it is read as a description. |
error | ReactNode | none | Error text under the label, in the danger colour. Sets aria-invalid on the input; pass id as well so it is read as a description. |
id | string | none | Optional. When set with description or error, the label, description and error get their own ids. |
className | string | none | Added to the root label, not the input. |
...rest | Omit<ComponentProps<"input">, "type"> | none | Every native input attribute and ref, applied to the input. type is always checkbox. |
Anatomy
| root | .eq-checkbox | The wrapping label, so the whole row is the click target. |
|---|---|---|
| box | .eq-checkbox-box | Holds the input and the marks drawn over it. |
| input | .eq-checkbox-input | The native checkbox with appearance: none. |
| check | .eq-checkbox-check | Shown when checked. |
| dash | .eq-checkbox-dash | Shown when indeterminate, in place of the check. |
| text | .eq-checkbox-text | |
| label | .eq-checkbox-label | |
| description | .eq-checkbox-description | |
| error | .eq-checkbox-error | Only when error is given. |
Keyboard
| Space | Toggles the checkbox. Provided by the browser. |
|---|
Notes
- Renders on the server with no client JavaScript; it is a native checkbox, so forms, defaultChecked and required work as usual.
- The indeterminate look follows the native indeterminate property, which only script can set; set it through a ref from a client component.
- Assistive tech reads an indeterminate native checkbox as mixed, with no extra ARIA.
- Without an id, a description stays part of the accessible name; with one, it is linked by aria-describedby instead. Not yet checked with a screen reader.
- An error sets aria-invalid and draws the box edge in the danger colour while unticked; the words, not the colour, carry the message.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/checkbox.tsx73 lines
import type { ComponentProps, ReactNode } from "react";
import "./checkbox.css";
/** True when there is something to render; false, null and "" count as nothing. */
function present(node: ReactNode): boolean {
return node != null && node !== false && node !== "";
}
export type CheckboxProps = Omit<ComponentProps<"input">, "type"> & {
/** Visible label. Clicking it toggles the box. */
label: ReactNode;
/** Secondary line under the label. Pass an id too, so it is read as a description, not as part of the name. */
description?: ReactNode;
/** Error text under the label. When set, the box is marked invalid; with an id, the text is announced as its description. */
error?: ReactNode;
};
export function Checkbox({
label,
description,
error,
id,
className,
"aria-labelledby": labelledBy,
"aria-describedby": describedBy,
"aria-invalid": ariaInvalid,
...rest
}: CheckboxProps) {
const invalid = present(error);
// With an id, the description and error are split out of the name; without one they stay inside the label.
const split = id != null && (present(description) || invalid);
const labelId = split ? `${id}-label` : undefined;
const descriptionId = split && present(description) ? `${id}-description` : undefined;
const errorId = split && invalid ? `${id}-error` : undefined;
const described = [describedBy, descriptionId, errorId].filter(Boolean).join(" ") || undefined;
return (
<label className={["eq-checkbox", className].filter(Boolean).join(" ")}>
<span className="eq-checkbox-box">
<input
{...rest}
type="checkbox"
id={id}
className="eq-checkbox-input"
aria-labelledby={labelId ?? labelledBy}
aria-describedby={described}
aria-invalid={invalid ? true : ariaInvalid}
/>
<svg className="eq-checkbox-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="m4.5 8.25 2.25 2.25 4.75-5" pathLength="1" />
</svg>
<svg className="eq-checkbox-dash" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true">
<path d="M4.75 8h6.5" pathLength="1" />
</svg>
</span>
<span className="eq-checkbox-text">
<span className="eq-checkbox-label" id={labelId}>
{label}
</span>
{present(description) && (
<span className="eq-checkbox-description" id={descriptionId}>
{description}
</span>
)}
{invalid && (
<span className="eq-checkbox-error" id={errorId}>
{error}
</span>
)}
</span>
</label>
);
}components/equip/checkbox.css127 lines
@layer equip {
.eq-checkbox {
display: inline-flex;
align-items: flex-start;
gap: var(--eq-space-2);
/* 2px above and below a 20px line keeps the whole row a 24px target. */
min-height: 24px;
box-sizing: border-box;
padding-block: 2px;
font: var(--eq-weight) var(--eq-text-m) / 20px var(--eq-font);
color: var(--eq-ink);
cursor: pointer;
}
.eq-checkbox-box {
position: relative;
flex: none;
display: block;
width: 16px;
height: 16px;
margin-top: 2px;
transition: scale var(--eq-dur) var(--eq-ease);
}
/* The box gives under the finger; at 16px it gives a little more than a button. */
.eq-checkbox:active:not(:has(.eq-checkbox-input:disabled)) .eq-checkbox-box {
scale: calc(var(--eq-press) - 0.04);
}
.eq-checkbox-input {
appearance: none;
box-sizing: border-box;
display: block;
width: 16px;
height: 16px;
margin: 0;
border: 1px solid var(--eq-line-strong);
border-radius: var(--eq-radius-s);
background: var(--eq-raised);
cursor: inherit;
transition:
background-color var(--eq-dur) var(--eq-ease),
border-color var(--eq-dur) var(--eq-ease);
}
.eq-checkbox-input:is(:checked, :indeterminate) {
border-color: var(--eq-solid);
background: var(--eq-solid);
}
.eq-checkbox-check,
.eq-checkbox-dash {
position: absolute;
inset: 0;
width: 16px;
height: 16px;
color: var(--eq-solid-ink);
pointer-events: none;
opacity: 0;
transition: opacity var(--eq-dur) var(--eq-ease);
}
/* Each mark is drawn along its stroke (the paths carry pathLength="1"). The dash is
twice the path and the gap longer still, so the spring can overshoot the end without
clipping the start, and no round cap peeks out while it is hidden. Leaving, it fades
and rewinds quickly. */
.eq-checkbox-check > path,
.eq-checkbox-dash > path {
stroke-dasharray: 2 3;
stroke-dashoffset: 2;
transition: stroke-dashoffset var(--eq-dur) var(--eq-ease);
}
.eq-checkbox-input:checked:not(:indeterminate) ~ .eq-checkbox-check,
.eq-checkbox-input:indeterminate ~ .eq-checkbox-dash {
opacity: 1;
}
.eq-checkbox-input:checked:not(:indeterminate) ~ .eq-checkbox-check > path,
.eq-checkbox-input:indeterminate ~ .eq-checkbox-dash > path {
stroke-dashoffset: 1;
transition: stroke-dashoffset var(--eq-dur-move) var(--eq-spring);
}
@media (hover: hover) {
.eq-checkbox:hover .eq-checkbox-input:where(:not(:checked, :indeterminate, :disabled)) {
border-color: color-mix(in srgb, var(--eq-line-strong), var(--eq-ink) 30%);
}
}
.eq-checkbox-input:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
/* A required box left unticked, once the person has tried to submit or touched it. */
.eq-checkbox .eq-checkbox-input:user-invalid {
border-color: var(--eq-danger);
}
/* An error from the caller marks an unticked box the same way. */
.eq-checkbox-input[aria-invalid="true"]:not(:checked, :indeterminate) {
border-color: var(--eq-danger);
}
.eq-checkbox:has(.eq-checkbox-input:disabled) {
opacity: 0.45;
cursor: not-allowed;
}
.eq-checkbox-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.eq-checkbox-description,
.eq-checkbox-error {
font-size: var(--eq-text-s);
line-height: 18px;
color: var(--eq-ink-2);
}
.eq-checkbox-error {
color: var(--eq-danger);
}
}