Skip to main content
On this pageOverview

Switch

Overview

An on/off toggle. Semantically different from Checkbox: Switch represents an immediate action (like a light switch), while Checkbox represents a form value that gets submitted. Switch uses the Submodel pattern with the same wiring as Checkbox.

See it in an app

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

Examples

The switch renders as a <button> with role="switch". The typical visual is a track with a sliding knob, styled with the data-checked attribute for the on state.

Get notified when something important happens.

// 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 Switch Submodel:
const Model = S.Struct({
  switchDemo: Ui.Switch.Model,
  // ...your other fields
})

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

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

// Inside your update function's M.tagsExhaustive({...}), delegate to
// Ui.Switch.update. The OutMessage's `ToggledChecked` carries the new
// `isChecked` value. Use it to save a preference, sync to a backend,
// or trigger a side effect at the toggle moment.
GotSwitchMessage: ({ message }) => {
  const [nextSwitch, commands, maybeOutMessage] = Ui.Switch.update(
    model.switchDemo,
    message,
  )
  const mappedCommands = Command.mapMessages(commands, message =>
    GotSwitchMessage({ message }),
  )

  return Option.match(maybeOutMessage, {
    onNone: () => [
      evo(model, { switchDemo: () => nextSwitch }),
      mappedCommands,
    ],
    onSome: M.type<Ui.Switch.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 persist the preference, fire
          // analytics, or dispatch a downstream Command.
          return [evo(model, { switchDemo: () => nextSwitch }), mappedCommands]
        },
      }),
    ),
  })
}

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

  return h.submodel({
    slotId: 'switch-demo',
    model: model.switchDemo,
    view: Ui.Switch.view,
    viewInputs: {
      toView: attributes =>
        h.div(
          [h.Class('flex items-center gap-3')],
          [
            h.button(
              [
                ...attributes.button,
                h.Class(
                  'relative h-6 w-11 rounded-full transition-colors data-[checked]:bg-blue-600 bg-gray-200',
                ),
              ],
              [
                h.div(
                  [
                    h.Class(
                      'absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform',
                    ),
                  ],
                  [],
                ),
              ],
            ),
            h.div(
              [],
              [
                h.label(
                  [...attributes.label, h.Class('text-sm font-medium')],
                  ['Enable notifications'],
                ),
                h.p(
                  [...attributes.description, h.Class('text-sm text-gray-500')],
                  ['Get notified when something important happens.'],
                ),
              ],
            ),
          ],
        ),
    },
    toParentMessage: message => GotSwitchMessage({ message }),
  })
}

Styling

Switch is headless. Your toView callback controls all markup and styling. Use data-[checked] to change the track color and translate the knob.

AttributeCondition
data-checkedPresent when the switch is on.
data-disabledPresent when isDisabled is true.

Keyboard Interaction

KeyDescription
SpaceToggles the switch.

Accessibility

The switch button receives role="switch" and aria-checked. The label is linked via aria-labelledby and the description via aria-describedby. Clicking the label toggles the switch.

API Reference

InitConfig

Configuration object passed to Switch.init().

NameTypeDefaultDescription
idstring-Unique ID for the switch instance.
isCheckedbooleanfalseInitial on/off state.

ViewConfig

Configuration object passed to Switch.view().

NameTypeDefaultDescription
modelSwitch.Model-The switch state from your parent Model.
toParentMessage(childMessage: Switch.Message) => ParentMessage-Wraps Switch Messages in your parent Message type for Submodel delegation.
toView(attributes: SwitchAttributes) => Html-Callback that receives attribute groups for the button, label, description, and hidden input elements.
isDisabledbooleanfalseWhether the switch is disabled.
namestring-Form field name. When provided, a hidden input is included for native form submission.
valuestring'on'Value sent in the form when checked.

SwitchAttributes

Attribute groups provided to the toView callback.

NameTypeDefaultDescription
buttonReadonlyArray<Attribute<Message>>-Spread onto the switch button element. 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 switch.
descriptionReadonlyArray<Attribute<Message>>-Spread onto a description element. Includes an id referenced by aria-describedby on the switch.
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 switch toggles. Carries the new checked state. Pattern-match the third tuple element of Switch.update in your GotSwitchMessage handler to lift the toggle into a domain Message (e.g., persisting the setting or dispatching a sync command).

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson