Skip to main content
On this pageOverview

Checkbox

Overview

A stateful toggle with checked, unchecked, and indeterminate states. Checkbox uses the Submodel pattern: initialize with Checkbox.init(), store the Model in your parent, delegate Messages via Checkbox.update(), and render with Checkbox.view(). For an on/off toggle that represents an immediate action (like a light switch), use Switch instead.

See it in an app

Check out how Checkbox is wired up in a real Foldkit app.

Examples

Basic

The checkbox element is typically a <button>. Spread attributes.checkbox onto it for role, ARIA state, and keyboard/click handlers. The label click handler also toggles the checkbox.

You agree to our Terms of Service and Privacy Policy.

// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt. Fit them into your own Model, init, Message,
// update, and view definitions.
import { Match as M, Option } from 'effect'
import { Command, Ui } from 'foldkit'
import { html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

// Add a field to your Model for the Checkbox Submodel:
const Model = S.Struct({
  checkboxDemo: Ui.Checkbox.Model,
  // ...your other fields
})

// In your init function, initialize the Checkbox Submodel with a unique id:
const init = () => [
  {
    checkboxDemo: Ui.Checkbox.init({ id: 'terms' }),
    // ...your other fields
  },
  [],
]

// Embed the Checkbox Message in your parent Message:
const GotCheckboxMessage = m('GotCheckboxMessage', {
  message: Ui.Checkbox.Message,
})

// Inside your update function's M.tagsExhaustive({...}), delegate to
// Ui.Checkbox.update. The OutMessage's `ToggledChecked` carries the new
// `isChecked` value. Use it to fire analytics, validate a form, or push
// the value to a backend at the toggle moment.
GotCheckboxMessage: ({ message }) => {
  const [nextCheckbox, commands, maybeOutMessage] = Ui.Checkbox.update(
    model.checkboxDemo,
    message,
  )
  const mappedCommands = Command.mapMessages(commands, message =>
    GotCheckboxMessage({ message }),
  )

  return Option.match(maybeOutMessage, {
    onNone: () => [
      evo(model, { checkboxDemo: () => nextCheckbox }),
      mappedCommands,
    ],
    onSome: M.type<Ui.Checkbox.OutMessage>().pipe(
      M.tagsExhaustive({
        ToggledChecked: ({ isChecked }) => {
          // The child has emitted `ToggledChecked`. The body commits
          // the child's next state as usual. In this arm the parent
          // can also update its own state or dispatch its own
          // Commands, for example save the preference, validate a
          // form, or dispatch a downstream Command.
          return [
            evo(model, { checkboxDemo: () => nextCheckbox }),
            mappedCommands,
          ]
        },
      }),
    ),
  })
}

// Inside your view function, render the checkbox via h.submodel:
const view = () => {
  const h = html<Message>()

  return h.submodel({
    slotId: 'terms-checkbox',
    model: model.checkboxDemo,
    view: Ui.Checkbox.view,
    viewInputs: {
      toView: attributes =>
        h.div(
          [h.Class('flex flex-col gap-1')],
          [
            h.div(
              [h.Class('flex items-center gap-2')],
              [
                h.button(
                  [...attributes.checkbox, h.Class('h-5 w-5 rounded border')],
                  model.checkboxDemo.isChecked ? ['✓'] : [],
                ),
                h.label(
                  [...attributes.label, h.Class('text-sm')],
                  ['Accept terms and conditions'],
                ),
              ],
            ),
            h.p(
              [...attributes.description, h.Class('text-sm text-gray-500')],
              ['You agree to our Terms of Service.'],
            ),
          ],
        ),
    },
    toParentMessage: message => GotCheckboxMessage({ message }),
  })
}

Indeterminate

Pass isIndeterminate: true to show a mixed state. This is typically computed from child checkbox states: when some but not all children are checked, the parent shows the indeterminate mark. Toggling the parent sets all children to the same state.

// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt. Fit them into your own Model, init, Message,
// update, and view definitions.
import { Array } from 'effect'
import { Ui } from 'foldkit'
import { html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

// Add multiple Checkbox Submodels to your Model for the parent and children:
const Model = S.Struct({
  optionA: Ui.Checkbox.Model,
  optionB: Ui.Checkbox.Model,
  // ...your other fields
})

// In your init function, initialize each Submodel:
const init = () => [
  {
    optionA: Ui.Checkbox.init({ id: 'option-a' }),
    optionB: Ui.Checkbox.init({ id: 'option-b' }),
    // ...your other fields
  },
  [],
]

// Embed each child's Message, plus a Message for the "Select All" parent:
const GotSelectAllMessage = m('GotSelectAllMessage', {
  message: Ui.Checkbox.Message,
})
const GotOptionAMessage = m('GotOptionAMessage', {
  message: Ui.Checkbox.Message,
})
const GotOptionBMessage = m('GotOptionBMessage', {
  message: Ui.Checkbox.Message,
})

// Inside your update function's M.tagsExhaustive({...}), toggling
// "Select All" routes each child through Ui.Checkbox.setChecked so the
// update goes through the Submodel rather than mutating its fields directly:
GotSelectAllMessage: () => {
  const isAllChecked = Array.every(
    [model.optionA, model.optionB],
    ({ isChecked }) => isChecked,
  )
  const nextChecked = !isAllChecked

  const [nextOptionA] = Ui.Checkbox.setChecked(model.optionA, nextChecked)
  const [nextOptionB] = Ui.Checkbox.setChecked(model.optionB, nextChecked)

  return [
    evo(model, {
      optionA: () => nextOptionA,
      optionB: () => nextOptionB,
    }),
    [],
  ]
}

// Compute the parent's indeterminate state from the child checkboxes:
const checkboxes = [model.optionA, model.optionB]
const isAllChecked = Array.every(checkboxes, ({ isChecked }) => isChecked)
const isIndeterminate =
  !isAllChecked && Array.some(checkboxes, ({ isChecked }) => isChecked)

// Inside your view function, pass isIndeterminate via h.submodel's viewInputs:
const view = () => {
  const h = html<Message>()

  return h.submodel({
    slotId: 'select-all',
    model: { id: 'select-all', isChecked: isAllChecked },
    view: Ui.Checkbox.view,
    viewInputs: {
      isIndeterminate,
      toView: attributes =>
        h.div(
          [h.Class('flex items-center gap-2')],
          [
            h.button(
              [...attributes.checkbox, h.Class('h-5 w-5 rounded border')],
              isIndeterminate ? ['—'] : isAllChecked ? ['✓'] : [],
            ),
            h.label(
              [...attributes.label, h.Class('text-sm')],
              ['All notifications'],
            ),
          ],
        ),
    },
    toParentMessage: message => GotSelectAllMessage({ message }),
  })
}

Styling

Checkbox is headless. Your toView callback controls all markup and styling. Use the data attributes below to style checked, indeterminate, and disabled states.

AttributeCondition
data-checkedPresent when checked and not indeterminate.
data-indeterminatePresent when isIndeterminate is true.
data-disabledPresent when isDisabled is true.

Keyboard Interaction

KeyDescription
SpaceToggles the checkbox.

Accessibility

The checkbox element receives role="checkbox" and aria-checked which is set to "true", "false", or "mixed" depending on the checked and indeterminate state. The label is linked via aria-labelledby and the description via aria-describedby.

API Reference

InitConfig

Configuration object passed to Checkbox.init().

NameTypeDefaultDescription
idstring-Unique ID for the checkbox instance.
isCheckedbooleanfalseInitial checked state.

Model

The checkbox state managed as a Submodel field in your parent Model.

NameTypeDefaultDescription
idstring-The checkbox instance ID.
isCheckedboolean-Whether the checkbox is currently checked.

ViewConfig

Configuration object passed to Checkbox.view().

NameTypeDefaultDescription
modelCheckbox.Model-The checkbox state from your parent Model.
toParentMessage(childMessage: Checkbox.Message) => ParentMessage-Wraps Checkbox Messages in your parent Message type for Submodel delegation.
toView(attributes: CheckboxAttributes) => Html-Callback that receives attribute groups for the checkbox, label, description, and hidden input elements.
isDisabledbooleanfalseWhether the checkbox is disabled.
isIndeterminatebooleanfalseWhether to show the indeterminate (mixed) state. Useful for "select all" checkboxes where some but not all children are checked.
namestring-Form field name. When provided, a hidden input is included for native form submission.
valuestring'on'Value sent in the form when checked.

CheckboxAttributes

Attribute groups provided to the toView callback.

NameTypeDefaultDescription
checkboxReadonlyArray<Attribute<Message>>-Spread onto the checkbox element (typically a <button>). Includes role, aria-checked, tabindex, and click/keyboard handlers.
labelReadonlyArray<Attribute<Message>>-Spread onto the label element. Includes an id for aria-labelledby and a click handler that toggles the checkbox.
descriptionReadonlyArray<Attribute<Message>>-Spread onto a description element. Includes an id referenced by aria-describedby on the checkbox.
hiddenInputReadonlyArray<Attribute<Message>>-Spread onto a hidden <input> for form submission. Only needed when the name prop is set.

OutMessage

Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Pattern-match on the OutMessage in your update handler.

NameTypeDefaultDescription
ToggledChecked{ isChecked: boolean }-Emitted each time the checkbox toggles. Carries the new checked state. Pattern-match the third tuple element of Checkbox.update in your GotCheckboxMessage handler to lift the toggle into a domain Message (e.g., persisting the flag or dispatching a save command).

Programmatic Helpers

Helpers a parent calls in its update without constructing a Checkbox Message.

NameTypeDefaultDescription
setChecked(model: Model, isChecked: boolean) => [Model, Commands, Option<OutMessage>]-Commits a checked state as a user-style choice, emitting ToggledChecked. Use for a programmatic change that should behave like a click. To mirror an external value without emitting, use reflectChecked.
reflectChecked(model: Model, isChecked: boolean) => Model-Reflects an externally-sourced checked state onto the model without emitting an OutMessage. Use to mirror external truth (saved settings, a server value, a sibling field) onto the checkbox. Dual: pass just the boolean for a point-free setter in an evo callback.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson