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

PropTypeDefaultWhat it does
labelReactNodenoneVisible label. Clicking it toggles the box.
descriptionReactNodenoneSecondary line under the label. Pass id as well so it is read as a description.
errorReactNodenoneError 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.
idstringnoneOptional. When set with description or error, the label, description and error get their own ids.
classNamestringnoneAdded to the root label, not the input.
...restOmit<ComponentProps<"input">, "type">noneEvery native input attribute and ref, applied to the input. type is always checkbox.

Anatomy

root.eq-checkboxThe wrapping label, so the whole row is the click target.
box.eq-checkbox-boxHolds the input and the marks drawn over it.
input.eq-checkbox-inputThe native checkbox with appearance: none.
check.eq-checkbox-checkShown when checked.
dash.eq-checkbox-dashShown when indeterminate, in place of the check.
text.eq-checkbox-text
label.eq-checkbox-label
description.eq-checkbox-description
error.eq-checkbox-errorOnly when error is given.

Keyboard

SpaceToggles 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);
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index