Checkbox

Form control primitive for boolean and mixed selection state. When omitting visible label text in a list or table, supply label to give the native checkbox an accessible name.

Default

With no state properties supplied, Checkbox starts unchecked and enabled.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>

<ui-checkbox>Notify Sam</ui-checkbox>
<script setup>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<template>
  <UiCheckbox>Notify Sam</UiCheckbox>
</template>
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox>Notify Sam</UiCheckbox>;
}
/** @jsxImportSource preact */
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox>Notify Sam</UiCheckbox>;
}
<script>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<UiCheckbox>Notify Sam</UiCheckbox>
import { Component } from "@angular/core";
import { UiCheckbox } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCheckbox],
  template: `
    <label
      ui-checkbox
    >Notify Sam</label>
  `,
})
export default class Example {}

Select all and mixed state

Change an item to see the parent return to a mixed state. Clicking the parent selects or clears the whole group.

Code

<script
  type="module"
>
  import "@threadlabs/looma";
  import { componentRegistry } from "@nextwebwg/html-next/runtime";

  const all = document.querySelector("#docs-checkbox-all");
  const items = [...document.querySelectorAll(".docs-checkbox-item")];
  const isChecked = (item) => item.querySelector('input[type="checkbox"]').checked;

  all.addEventListener("change", (event) => {
    if (typeof event.detail?.checked !== "boolean") return;
    for (const item of items) componentRegistry.update(item, { checked: event.detail.checked });
    componentRegistry.update(all, { indeterminate: false });
  });

  for (const item of items) item.addEventListener("change", (event) => {
    if (typeof event.detail?.checked !== "boolean") return;
    const count = items.filter(isChecked).length;
    componentRegistry.update(all, { checked: count === items.length, indeterminate: count > 0 && count < items.length });
  });
</script>

<ui-stack
  gap="s"
>
  <ui-checkbox
    id="docs-checkbox-all"
    indeterminate
  >Select all</ui-checkbox>
  <ui-stack
    gap="xs"
    style="padding-inline-start: 1.5rem"
  >
    <ui-checkbox
      class="docs-checkbox-item"
      checked
    >Design</ui-checkbox>
    <ui-checkbox
      class="docs-checkbox-item"
    >Build</ui-checkbox>
    <ui-checkbox
      class="docs-checkbox-item"
    >Review</ui-checkbox>
  </ui-stack>
</ui-stack>
<script setup>
  import { UiCheckbox, UiStack } from "@threadlabs/looma";
</script>

<template>
  <UiStack
    gap="s"
  >
    <UiCheckbox
      id="docs-checkbox-all"
      data-example-id="docs-checkbox-all"
      :indeterminate="true"
    >Select all</UiCheckbox>
    <UiStack
      gap="xs"
      style="padding-inline-start: 1.5rem"
    >
      <UiCheckbox
        class="docs-checkbox-item"
        :checked="true"
      >Design</UiCheckbox>
      <UiCheckbox
        class="docs-checkbox-item"
      >Build</UiCheckbox>
      <UiCheckbox
        class="docs-checkbox-item"
      >Review</UiCheckbox>
    </UiStack>
  </UiStack>
</template>
import { UiCheckbox, UiStack } from "@threadlabs/looma";

export default function Example() {
  return <UiStack
    gap="s"
  >
    <UiCheckbox
      id="docs-checkbox-all"
      data-example-id="docs-checkbox-all"
      indeterminate
    >Select all</UiCheckbox>
    <UiStack
      gap="xs"
      style={{"paddingInlineStart":"1.5rem"}}
    >
      <UiCheckbox
        className="docs-checkbox-item"
        checked
      >Design</UiCheckbox>
      <UiCheckbox
        className="docs-checkbox-item"
      >Build</UiCheckbox>
      <UiCheckbox
        className="docs-checkbox-item"
      >Review</UiCheckbox>
    </UiStack>
  </UiStack>;
}
/** @jsxImportSource preact */
import { UiCheckbox, UiStack } from "@threadlabs/looma";

export default function Example() {
  return <UiStack
    gap="s"
  >
    <UiCheckbox
      id="docs-checkbox-all"
      data-example-id="docs-checkbox-all"
      indeterminate
    >Select all</UiCheckbox>
    <UiStack
      gap="xs"
      style={{"paddingInlineStart":"1.5rem"}}
    >
      <UiCheckbox
        className="docs-checkbox-item"
        checked
      >Design</UiCheckbox>
      <UiCheckbox
        className="docs-checkbox-item"
      >Build</UiCheckbox>
      <UiCheckbox
        className="docs-checkbox-item"
      >Review</UiCheckbox>
    </UiStack>
  </UiStack>;
}
<script>
  import { UiCheckbox, UiStack } from "@threadlabs/looma";
</script>

<UiStack
  gap="s"
>
  <UiCheckbox
    id="docs-checkbox-all"
    data-example-id="docs-checkbox-all"
    indeterminate
  >Select all</UiCheckbox>
  <UiStack
    gap="xs"
    style="padding-inline-start: 1.5rem"
  >
    <UiCheckbox
      class="docs-checkbox-item"
      checked
    >Design</UiCheckbox>
    <UiCheckbox
      class="docs-checkbox-item"
    >Build</UiCheckbox>
    <UiCheckbox
      class="docs-checkbox-item"
    >Review</UiCheckbox>
  </UiStack>
</UiStack>
import { Component } from "@angular/core";
import { UiCheckbox, UiStack } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiStack, UiCheckbox],
  template: `
    <div
      ui-stack
      [gap]="'s'"
    >
      <label
        ui-checkbox
        id="docs-checkbox-all"
        data-example-id="docs-checkbox-all"
        [indeterminate]="true"
      >Select all</label>
      <div
        ui-stack
        [gap]="'xs'"
        style="padding-inline-start: 1.5rem"
      >
        <label
          ui-checkbox
          class="docs-checkbox-item"
          [checked]="true"
        >Design</label>
        <label
          ui-checkbox
          class="docs-checkbox-item"
        >Build</label>
        <label
          ui-checkbox
          class="docs-checkbox-item"
        >Review</label>
      </div>
    </div>
  `,
})
export default class Example {}

Multi-line label

The control stays aligned to the first line while long label text wraps at a constrained width.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>

<div
  style="max-inline-size: 18rem"
>
  <ui-checkbox>Send me product updates, release notes, and occasional research invitations.</ui-checkbox>
</div>
<script setup>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<template>
  <div
    style="max-inline-size: 18rem"
  >
    <UiCheckbox>Send me product updates, release notes, and occasional research invitations.</UiCheckbox>
  </div>
</template>
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxInlineSize":"18rem"}}
  >
    <UiCheckbox>Send me product updates, release notes, and occasional research invitations.</UiCheckbox>
  </div>;
}
/** @jsxImportSource preact */
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <div
    style={{"maxInlineSize":"18rem"}}
  >
    <UiCheckbox>Send me product updates, release notes, and occasional research invitations.</UiCheckbox>
  </div>;
}
<script>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<div
  style="max-inline-size: 18rem"
>
  <UiCheckbox>Send me product updates, release notes, and occasional research invitations.</UiCheckbox>
</div>
import { Component } from "@angular/core";
import { UiCheckbox } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCheckbox],
  template: `
    <div
      style="max-inline-size: 18rem"
    >
      <label
        ui-checkbox
      >Send me product updates, release notes, and occasional research invitations.</label>
    </div>
  `,
})
export default class Example {}

name, value, and required

Name and value are what the form submits when the box is checked (value defaults to "on"), and an unchecked box sends nothing; required blocks submission until it is checked.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>

<form>
  <ui-checkbox
    name="terms"
    value="accepted"
    required
  >I accept the terms</ui-checkbox>
</form>
<script setup>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<template>
  <form>
    <UiCheckbox
      name="terms"
      value="accepted"
      :required="true"
    >I accept the terms</UiCheckbox>
  </form>
</template>
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <form>
    <UiCheckbox
      name="terms"
      value="accepted"
      required
    >I accept the terms</UiCheckbox>
  </form>;
}
/** @jsxImportSource preact */
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <form>
    <UiCheckbox
      name="terms"
      value="accepted"
      required
    >I accept the terms</UiCheckbox>
  </form>;
}
<script>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<form>
  <UiCheckbox
    name="terms"
    value="accepted"
    required
  >I accept the terms</UiCheckbox>
</form>
import { Component } from "@angular/core";
import { UiCheckbox } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCheckbox],
  template: `
    <form>
      <label
        ui-checkbox
        [name]="'terms'"
        [value]="'accepted'"
        [required]="true"
      >I accept the terms</label>
    </form>
  `,
})
export default class Example {}

With a description

The description slot says what the choice means, on a line under its label. It is the input's accessible description, not part of its name.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>

<ui-checkbox
  name="digest"
  checked
>
  Weekly digest
  <span
    slot="description"
  >A summary of what changed in your workspaces, every Monday.</span>
</ui-checkbox>
<script setup>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<template>
  <UiCheckbox
    name="digest"
    :checked="true"
  >Weekly digest<template #description>
      <span>A summary of what changed in your workspaces, every Monday.</span>
    </template></UiCheckbox>
</template>
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox
    name="digest"
    checked
    slots={{
      description: <span>A summary of what changed in your workspaces, every Monday.</span>,
    }}
  >Weekly digest</UiCheckbox>;
}
/** @jsxImportSource preact */
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox
    name="digest"
    checked
    slots={{
      description: <span>A summary of what changed in your workspaces, every Monday.</span>,
    }}
  >Weekly digest</UiCheckbox>;
}
<script>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

{#snippet description()}
  <span>A summary of what changed in your workspaces, every Monday.</span>
{/snippet}

<UiCheckbox
  name="digest"
  checked
  slots={{ description }}
>Weekly digest</UiCheckbox>
import { Component } from "@angular/core";
import { UiCheckbox } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCheckbox],
  template: `
    <label
      ui-checkbox
      [name]="'digest'"
      [checked]="true"
    >Weekly digest<ng-container ngProjectAs="[slot='description']">
        <span>A summary of what changed in your workspaces, every Monday.</span>
      </ng-container></label>
  `,
})
export default class Example {}

size

size="sm" keeps the checkbox's box and centres its label in the small control height, in the small controls' type size, so it lines up with a small button.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-cluster/ui-cluster.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>

<ui-cluster
  gap="s"
  align="center"
>
  <ui-checkbox
    size="sm"
  >Checkbox</ui-checkbox>
  <ui-button
    size="sm"
  >Small button</ui-button>
</ui-cluster>
<script setup>
  import { UiButton, UiCheckbox, UiCluster } from "@threadlabs/looma";
</script>

<template>
  <UiCluster
    gap="s"
    align="center"
  >
    <UiCheckbox
      size="sm"
    >Checkbox</UiCheckbox>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>
</template>
import { UiButton, UiCheckbox, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="center"
  >
    <UiCheckbox
      size="sm"
    >Checkbox</UiCheckbox>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>;
}
/** @jsxImportSource preact */
import { UiButton, UiCheckbox, UiCluster } from "@threadlabs/looma";

export default function Example() {
  return <UiCluster
    gap="s"
    align="center"
  >
    <UiCheckbox
      size="sm"
    >Checkbox</UiCheckbox>
    <UiButton
      size="sm"
    >Small button</UiButton>
  </UiCluster>;
}
<script>
  import { UiButton, UiCheckbox, UiCluster } from "@threadlabs/looma";
</script>

<UiCluster
  gap="s"
  align="center"
>
  <UiCheckbox
    size="sm"
  >Checkbox</UiCheckbox>
  <UiButton
    size="sm"
  >Small button</UiButton>
</UiCluster>
import { Component } from "@angular/core";
import { UiButton, UiCheckbox, UiCluster } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCluster, UiCheckbox, UiButton],
  template: `
    <div
      ui-cluster
      [gap]="'s'"
      [align]="'center'"
    >
      <label
        ui-checkbox
        [size]="'sm'"
      >Checkbox</label>
      <ng-template
        ui-button
        [size]="'sm'"
      >Small button</ng-template>
    </div>
  `,
})
export default class Example {}

Invalid choice

Invalid marks a choice whose validation is managed by the form and uses the field danger colour.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-checkbox/ui-checkbox.html"
>

<ui-checkbox
  invalid
  name="consent"
>I agree to continue</ui-checkbox>
<script setup>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<template>
  <UiCheckbox
    :invalid="true"
    name="consent"
  >I agree to continue</UiCheckbox>
</template>
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox
    invalid
    name="consent"
  >I agree to continue</UiCheckbox>;
}
/** @jsxImportSource preact */
import { UiCheckbox } from "@threadlabs/looma";

export default function Example() {
  return <UiCheckbox
    invalid
    name="consent"
  >I agree to continue</UiCheckbox>;
}
<script>
  import { UiCheckbox } from "@threadlabs/looma";
</script>

<UiCheckbox
  invalid
  name="consent"
>I agree to continue</UiCheckbox>
import { Component } from "@angular/core";
import { UiCheckbox } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiCheckbox],
  template: `
    <label
      ui-checkbox
      [invalid]="true"
      [name]="'consent'"
    >I agree to continue</label>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: label

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
checkedWhether the checkbox is checked initially or when set externally. A form reset returns it to this.checkedbooleanfalse—
disabledDisables the native checkbox, so it cannot be toggled or focused.disabledbooleanfalse—
indeterminateShows the mixed state, for a checkbox that summarises a group whose items are partly checked. It sets the native indeterminate property, which is independent of checked, so update it as the group changes.indeterminatebooleanfalse—
invalidExposes an externally validated error and styles the checkbox with the field danger colour.invalidbooleanfalse—
labelAn accessible name for a checkbox without a visible label slot. It names the native input.labelstring—
nameThe native name the checkbox submits under: a form that contains it sends name=value while it is checked, and nothing while it is unchecked, as a native checkbox does. Left empty, it takes no part in form submission.namestring—
requiredMarks the native checkbox required, so native form validation treats it as invalid until it is checked.requiredbooleanfalse—
sizeAligns the checkbox with fields and buttons of the same size. It keeps its box size; the label takes that size's field type size and its first line centres in that control height (sm 2rem, lg 3rem by default), so in a row of inputs, selects, and buttons of one size it shares their height and their text baseline. Touch input keeps the same visible alignment. md is the standard layout.sizekeywordmdsm, md, lg
valueThe value a form submits under name while the checkbox is checked, also reported in the change event's detail so one handler can tell checkboxes apart. on by default, as on a native checkbox.valuestringon—

Framework props

NameTypeDefaultAllowed values
checkedbooleanfalse—
disabledbooleanfalse—
indeterminatebooleanfalse—
invalidbooleanfalse—
labelstring—
namestring—
requiredbooleanfalse—
size"sm" | "md" | "lg"mdsm, md, lg
valuestringon—

Events

NameDetailDescription
changeobject
  • checked: boolean (required)
  • value: string (required)
  • trigger: keyword (required)
Emitted when a toggleable control changes checked state.

Slots

NamePurpose
defaultDefault child content.
descriptionNamed content for the description region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-checkbox-gap—var(--ui-space-2)
--ui-checkbox-invalid-border—var(--ui-field-danger, var(--ui-danger-solid))
--ui-checkbox-size—var(--ui-choice-mark-size) | 1.125rem

Shared tokens

TokenFallback
--ui-accent-highlightnone
--ui-accent-solid—
--ui-border-strong—
--ui-border-width—
--ui-choice-hover-highlightnone
--ui-choice-hover-surface—
--ui-choice-mark-size—
--ui-control-bordervar(--ui-border-strong)
--ui-control-border-hovervar(--ui-border-strong)
--ui-control-inset—
--ui-control-size-lg—
--ui-control-size-sm—
--ui-control-surfacevar(--ui-surface-default)
--ui-danger-solid—
--ui-disabled-border—
--ui-disabled-surface—
--ui-disabled-text—
--ui-field-dangervar(--ui-danger-solid)
--ui-focus-ringvar(--ui-accent-solid)
--ui-focus-width—
--ui-font-size-md—
--ui-font-size-sm—
--ui-line-height—
--ui-line-height-md—
--ui-motion-base—
--ui-motion-ease—
--ui-motion-ease-outcubic-bezier(0.16, 1, 0.3, 1)
--ui-motion-fast—
--ui-neutral-wash-hover—
--ui-radius-round—
--ui-radius-sm—
--ui-space-1—
--ui-space-2—
--ui-surface-default—
--ui-surface-highlightnone | linear-gradient(transparent, transparent)
--ui-text-on-accent—
--ui-text-primary—
--ui-text-secondary—