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 { 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 }),
})
}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 }),
})
}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. |
Messages emitted to the parent through the third element of [Model, Commands, Option<OutMessage>]. Pattern-match on the OutMessage in your update handler.
| Name | Type | Default | Description |
|---|---|---|---|
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). |
Helpers a parent calls in its update without constructing a Checkbox Message.
| Name | Type | Default | Description |
|---|---|---|---|
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. |