Skip to content
ShineUI
Install

Checkbox

Yes, no, or somewhere between.

<sui-checkbox>SelectionStable

About

Navy checkbox with checked and indeterminate states, a label and an optional description line. A hidden native input carries focus, Space and the screen-reader semantics; the drawn box is what the user sees. Use it alone for a yes or no question such as consent, or inside sui-checkbox-group for a set. Form-associated: posts name=value while checked.

Usage

<sui-checkbox label="Email me a copy of the invoice" name="email-copy" value="yes"></sui-checkbox> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-checkbox"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiCheckbox } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiCheckbox label="Email me a copy of the invoice" name="email-copy" value="yes" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiCheckbox } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiCheckbox label="Email me a copy of the invoice" name="email-copy" value="yes" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiCheckbox } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiCheckbox], template: `<sui-checkbox label="Email me a copy of the invoice" name="email-copy" value="yes" (suiChange)="onChange($event)"></sui-checkbox>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

A hidden native input carries focus, Space toggling and the screen-reader semantics; the drawn box is what the user sees. The label element wraps both, so clicking the text toggles the box.

<sui-checkbox label="Email me a copy of the invoice" name="email-copy" value="yes"></sui-checkbox>

Indeterminate and disabled

indeterminate draws a dash and announces aria-checked="mixed", and the first toggle clears it. A description is wired through aria-describedby, and invalid is the danger border alone, for a form that draws its summary elsewhere.

<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-checkbox label="Keep me signed in" checked></sui-checkbox> <sui-checkbox label="Select all" indeterminate></sui-checkbox> <sui-checkbox label="Include voided invoices" description="Voided documents stay in the audit trail."></sui-checkbox> <sui-checkbox label="Lock the period on close" invalid></sui-checkbox> <sui-checkbox label="Locked by policy" disabled checked></sui-checkbox> </div>

Sizes and unlabeled boxes

md is 18px and sm is 16px. A box with no visible label takes its accessible name from the host aria-label, which is what a table selection column needs.

<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-checkbox label="Send a reminder 7 days before due" size="md" checked></sui-checkbox> <sui-checkbox label="Send a reminder on the due date" size="sm" checked></sui-checkbox> <sui-checkbox size="sm" aria-label="Select invoice INV-1043"></sui-checkbox> </div>

States

A consent box is a field on its own, so it draws its own message line: help sits under the box, and error replaces it, paints the danger border and sets aria-invalid. The message renders outside the label element, so clicking it does not tick the box.

<div style="display:grid;gap:16px;max-width:420px"> <sui-checkbox label="Email me a copy of the invoice" help="Sent to the billing address on the customer record."></sui-checkbox> <sui-checkbox label="I accept the engagement letter" required help="Required before we can file."></sui-checkbox> <sui-checkbox label="I accept the engagement letter" required error="Accept the engagement letter to continue."></sui-checkbox> </div>

Read-only

readonly keeps the box named and in the tab order, a click or Space changes nothing, and it still posts; a disabled box posts nothing. Neither holds the form on valueMissing.

<div style="display:grid;gap:16px;max-width:420px"> <sui-checkbox label="Engagement letter accepted" name="letter" checked readonly help="Accepted on 12 Aug by the client; a new letter is a new record."></sui-checkbox> </div>

In a form

A checked box posts name=value, where value defaults to "on", and an unchecked box posts nothing, the way a native checkbox does. A required box that is not ticked holds the form on valueMissing, and the browser names the field from its label.

Submit to see what posts.
<form id="cb-form" style="display:grid;gap:12px;justify-items:start;max-width:380px"> <sui-checkbox name="reminders" value="yes" label="Send payment reminders" checked></sui-checkbox> <sui-checkbox name="attach" label="Attach the PDF" description="The invoice is attached to every email."></sui-checkbox> <sui-checkbox name="terms" required label="I accept the deposit terms"></sui-checkbox> <sui-button type="submit" variant="brand" label="Save settings"></sui-button> <output id="cb-form-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#cb-form'); const out = root.querySelector('#cb-form-out'); form.addEventListener('submit', (e) => { e.preventDefault(); const entries = [...new FormData(form)].map(([k, v]) => `${k}=${v}`); out.textContent = entries.length ? entries.join(' ') : 'Nothing posted.'; }); }

Events

suiChange carries the new checked state. Here the header box is driven from the rows: all checked sets checked, some checked sets indeterminate.

Nothing selected.
<div style="display:flex;flex-direction:column;gap:12px;align-items:flex-start"> <sui-checkbox id="cb-all" label="Select all invoices"></sui-checkbox> <div id="cb-rows" style="display:flex;flex-direction:column;gap:10px;padding-left:22px"> <sui-checkbox class="cb-row" label="INV-1043 Silver Hills Church" value="INV-1043"></sui-checkbox> <sui-checkbox class="cb-row" label="INV-1044 Riverbend Dental Group" value="INV-1044"></sui-checkbox> <sui-checkbox class="cb-row" label="INV-1045 Clark County Schools" value="INV-1045"></sui-checkbox> </div> <span id="cb-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Nothing selected.</span> </div>
// setup(root): properties and events setup(root) { const all = root.querySelector('#cb-all'); const rows = [...root.querySelectorAll('.cb-row')]; const log = root.querySelector('#cb-log'); const sync = () => { const on = rows.filter((r) => r.checked); all.checked = on.length === rows.length; all.indeterminate = on.length > 0 && on.length < rows.length; log.textContent = on.length ? `Selected ${on.map((r) => r.value).join(', ')}.` : 'Nothing selected.'; }; all.addEventListener('suiChange', (e) => { rows.forEach((r) => { r.checked = e.detail; }); sync(); }); rows.forEach((r) => r.addEventListener('suiChange', sync)); }

Keyboard

KeyAction
TabMoves to the box; the drawn box shows the focus ring.
SpaceToggles the box and fires suiChange; on a mixed box the first press clears the dash.
EnterSubmits the surrounding form, as a native checkbox does.

API

<sui-checkbox>

Properties

PropertyAttributeTypeDefaultDescription
checkedcheckedbooleanfalseChecked state. Mutable: the element toggles it on user input, then emits suiChange. Reflected, so sui-checkbox[checked] matches the box as it is now.
descriptiondescriptionstringstring | undefinedSecondary line under the label explaining the consequence.
disableddisabledbooleanfalseDisables the box. A disabled box posts nothing and never holds the form.
errorerrorstringstring | undefinedError message under the box; also paints the danger border and sets aria-invalid. Inside a sui-checkbox-group the group owns the message line instead.
helphelpstringstring | undefinedHelp text under the box. Replaced by error when set.
indeterminateindeterminatebooleanfalseMixed state for select-all headers: drawn as a dash, announced as "mixed". The first toggle clears it.
invalidinvalidbooleanfalseDanger border without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedVisible label.
namenamestringstring | undefinedForm control name. Reflected: the form entry is built from the content attribute.
readOnlyreadonlybooleanfalseDisplay only: the box keeps its name and its tab stop but a click or Space changes nothing, and it still posts. Neither a read-only nor a disabled box holds the form on valueMissing.
requiredrequiredbooleanfalseMust be checked for the surrounding form to be valid, as a consent box is.
sizesize"md" | "sm"'md'Box size: md 18px (default), sm 16px.
valuevaluestring'on'Value posted with the form while checked; defaults to "on". Inside a sui-checkbox-group the group posts instead and this box posts nothing.

Events

EventDetailReactVueDescription
suiChangebooleanonSuiChange@sui-changeFires on toggle; detail is the new checked state.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the (hidden) native input, as el.focus() does; the drawn box shows the focus ring.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also