Checkbox

A pixel checkbox with a label, a mixed state and the command contract.

zweiundeins <sb-checkbox> checkbox boolean form indeterminate Since 2026-09-29 1.7 kB0Open in playground Edit on GitHub

Playground

A checkbox with a pixel box and a label, for a yes or no that belongs in a form. It keeps a local checked, emits sb-change when the user flips it, and follows the command contract, so it can drive a command as it is. For several choices under one name, use sb-checkbox-group.

Examples

Basic

<div style="display: grid; justify-items: start">
  <sb-checkbox label="Shields" checked></sb-checkbox>
  <sb-checkbox label="Cloaking device"></sb-checkbox>
  <sb-checkbox label="Self-destruct" disabled></sb-checkbox>
</div>

Mixed

indeterminate shows a dash: the server says "some", for example when some of the items below are checked and others aren't. It is the server's view state, not a value. A click resolves it to checked, and the server's next indeterminate wins again.

<sb-checkbox label="All cargo bays" indeterminate></sb-checkbox>

Two-way binding

The live state is the checked property, so data-bind works with the __prop and __event modifiers. Declare the signal first.

<div data-signals:_beacon="false">
  <sb-checkbox label="Distress beacon" data-bind:_beacon__prop.checked__event.change></sb-checkbox>
  <p data-text="$_beacon ? 'Beacon on' : 'Beacon off'"></p>
</div>

With commands

Give it a name, and it emits sb-change with { name, value } when the user flips it (value is the checked state): ready to post as a command. With confirm, it sets :state(pending) until the server's re-rendered checked matches, and revert() goes back to the server's state when a command is rejected.

<sb-checkbox name="beacon" confirm label="Distress beacon"
  data-on:sb-change="@post('/cmd/beacon', {payload: {tabid: $tabid, ...evt.detail}})"
  data-on:datastar-fetch="evt.detail.el === el && evt.detail.type === 'error' && el.revert()"></sb-checkbox>

Style the wait from the page:

sb-checkbox:state(pending) { opacity: 0.7; }

The server's checked wins when it sends a new one, even onto a checkbox rendered without it; the same markup again leaves the user's click alone. A removed attribute changes nothing, so send checked="false" to clear it. indeterminate is the server's too: a new one wins, a removed one clears the dash, and revert() brings back both. Write booleans as checked, checked="true" or checked="false". The live state before the echo and the pending state live in local signals and are never reflected to attributes. See Commands and components and the Showcase.

Forms

Inside a <form>, sb-checkbox submits like a native checkbox: name=value while it is checked (value defaults to on), nothing when it isn't, nothing without a name or while disabled. The mixed state doesn't change what is submitted. new FormData(form) and Datastar's contentType: 'form' include it, and a form reset brings back the server's state without change events. It is not a form-associated element yet (Rocket can't declare one), so required and validity, <fieldset disabled>, <label for> and the form attribute don't reach it. With commands, sb-change carries { name, value } (see With commands).

Styling

Parts: base (the clickable row), box and label. A checked box is --sb-brand in a --sb-brand-light frame with a --sb-text-on-brand mark; an empty one is --sb-control-bg in --sb-control-border (--sb-control-border-hover under the pointer, on a --sb-surface-hover row). The label is --sb-control-text at 0.875rem in the page's font. The corners come from --sb-notch, so [data-sb-style="smooth"] rounds them instead. A disabled checkbox has :state(disabled), from the decoded prop, so disabled="false" is not disabled.

<sb-checkbox label="Gold plating" checked style="--sb-brand: #F5C451; --sb-brand-light: #FFE08A; --sb-text-on-brand: #1B1300"></sb-checkbox>

Accessibility

The row is a role="checkbox" with aria-checked (mixed for the dash) and one tab stop; Space checks and clears it. A disabled checkbox has aria-disabled and leaves the tab order. The label names it. Without one, give <sb-checkbox> an aria-label: the box takes it over (aria-labelledby doesn't reach inside). In forced colours (Windows High Contrast) the checked box uses the system's Highlight colour and the mark HighlightText.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later.

<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>

<sb-checkbox name="shields" label="Shields"></sb-checkbox>

No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.

Load just this component, pinned to this version: the minified module with its integrity hash, so the browser refuses it if a single byte changes.

<script type="importmap">
  {
    "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": {
      "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG",
      "https://starbase.zweiundeins.gmbh/c/checkbox@2558efb584c2/checkbox.min.js": "sha384-92FhAhevBrxazXiNH7RViuusgKIRhmbzq5z2i8WUxg/HzXu2HRBhZO8Ve0rkFIY6"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/checkbox@2558efb584c2/checkbox.min.js" integrity="sha384-92FhAhevBrxazXiNH7RViuusgKIRhmbzq5z2i8WUxg/HzXu2HRBhZO8Ve0rkFIY6"></script>

<sb-checkbox name="shields" label="Shields"></sb-checkbox>

In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.

<script type="importmap">
  {
    "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": {
      "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG",
      "https://starbase.zweiundeins.gmbh/c/checkbox@2558efb584c2/checkbox.min.js": "sha384-92FhAhevBrxazXiNH7RViuusgKIRhmbzq5z2i8WUxg/HzXu2HRBhZO8Ve0rkFIY6"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@5fa59f8dc44a/autoloader.js" integrity="sha384-uIkM/3WJfrJGqPA5t7gqz6wwMDPADCPt9hxjY8Iax8QzMQbEnlfRNoTA5xCgZcTU"></script>

<sb-checkbox name="shields" label="Shields"></sb-checkbox>

Components import only datastar and their own files, so copying the folder is enough: save these files, keeping each component's folder, and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).

  • <sb-checkbox>
<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/checkbox/checkbox.min.js"></script>

<sb-checkbox name="shields" label="Shields"></sb-checkbox>

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotliminified
checkbox.js 10.0 kB4.0 kB3.4 kB1.7 kB

API reference

Props

AttributeTypeDefaultDescription
checkedbooleanfalseChecked or not. A new state from the server wins (send checked="false" to clear it); the live state is the checked property.
indeterminatebooleanfalseShows the mixed state (the server says "some"). A click resolves it to checked; the live state is the indeterminate property.
labelstring""Visible label next to the box, and its accessible name.
disabledbooleanfalseDisable interaction.
namestring""Name reported in sb-change (e.g. the field of a command), and the field it submits in a form.
valuestring"on"What a form submits under name while checked, like a native checkbox.
confirmbooleanfalseServer-confirmed value: :state(pending) while the local state differs from the server's checked attribute (see revert()).

Events

NameDescription
changeAfter the user checks or clears the box.
sb-changeSame moment. detail: { name, value, checked } (value is the checked state): ready for a command.