On this pageOverview
Checkbox
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.
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 { Effect } from 'effect'
import { Command, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, button, div, label, p } from './html'
// 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 Checkbox.update:
GotCheckboxMessage: ({ message }) => {
const [nextCheckbox, commands] = Ui.Checkbox.update(
model.checkboxDemo,
message,
)
return [
// Merge the next state into your Model:
evo(model, { checkboxDemo: () => nextCheckbox }),
// Forward the Submodel's Commands through your parent Message:
commands.map(
Command.mapEffect(Effect.map(message => GotCheckboxMessage({ message }))),
),
]
}
// Inside your view function, render the checkbox:
Ui.Checkbox.view({
model: model.checkboxDemo,
toParentMessage: message => GotCheckboxMessage({ message }),
toView: attributes =>
div(
[Class('flex flex-col gap-1')],
[
div(
[Class('flex items-center gap-2')],
[
button(
[...attributes.checkbox, Class('h-5 w-5 rounded border')],
model.checkboxDemo.isChecked ? ['✓'] : [],
),
label(
[...attributes.label, Class('text-sm')],
['Accept terms and conditions'],
),
],
),
p(
[...attributes.description, Class('text-sm text-gray-500')],
['You agree to our Terms of Service.'],
),
],
),
})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 { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, button, div, label } from './html'
// 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" sets all children to the same state:
GotSelectAllMessage: () => {
const isAllChecked = Array.every(
[model.optionA, model.optionB],
({ isChecked }) => isChecked,
)
const nextChecked = !isAllChecked
return [
evo(model, {
optionA: () => evo(model.optionA, { isChecked: () => nextChecked }),
optionB: () => evo(model.optionB, { isChecked: () => nextChecked }),
}),
[],
]
}
// 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 to the parent checkbox:
Ui.Checkbox.view({
model: { id: 'select-all', isChecked: isAllChecked },
isIndeterminate,
toParentMessage: message => GotSelectAllMessage({ message }),
toView: attributes =>
div(
[Class('flex items-center gap-2')],
[
button(
[...attributes.checkbox, Class('h-5 w-5 rounded border')],
isIndeterminate ? ['—'] : isAllChecked ? ['✓'] : [],
),
label([...attributes.label, Class('text-sm')], ['All notifications']),
],
),
})Checkbox is headless — your toView callback controls all markup and styling. Use the data attributes below to style checked, indeterminate, and disabled states.
| Attribute | Condition |
|---|---|
data-checked | Present when checked and not indeterminate. |
data-indeterminate | Present when isIndeterminate is true. |
data-disabled | Present when isDisabled is true. |
| Key | Description |
|---|---|
| Space | Toggles the checkbox. |
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.
Configuration object passed to Checkbox.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the checkbox instance. |
isChecked | boolean | false | Initial checked state. |
The checkbox state managed as a Submodel field in your parent Model.
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | The checkbox instance ID. |
isChecked | boolean | - | Whether the checkbox is currently checked. |
Configuration object passed to Checkbox.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | Checkbox.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. |
isDisabled | boolean | false | Whether the checkbox is disabled. |
isIndeterminate | boolean | false | Whether to show the indeterminate (mixed) state. Useful for "select all" checkboxes where some but not all children are checked. |
name | string | - | Form field name. When provided, a hidden input is included for native form submission. |
value | string | 'on' | Value sent in the form when checked. |
Attribute groups provided to the toView callback.
| Name | Type | Default | Description |
|---|---|---|---|
checkbox | ReadonlyArray<Attribute<Message>> | - | Spread onto the checkbox element (typically a <button>). Includes role, aria-checked, tabindex, and click/keyboard handlers. |
label | ReadonlyArray<Attribute<Message>> | - | Spread onto the label element. Includes an id for aria-labelledby and a click handler that toggles the checkbox. |
description | ReadonlyArray<Attribute<Message>> | - | Spread onto a description element. Includes an id referenced by aria-describedby on the checkbox. |
hiddenInput | ReadonlyArray<Attribute<Message>> | - | Spread onto a hidden <input> for form submission. Only needed when the name prop is set. |