Styled checkbox with label, indeterminate state and description, built on the @foldkit/ui Checkbox helper.

Preview
Style
Basic
With Description

By clicking this checkbox, you agree to the terms and conditions.

Invalid
Disabled
With Title
Group
src/demo/views/checkbox.ts282 lines
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { checkbox } from '../../generated/registry/ui/checkbox'
import {
  field,
  fieldContent,
  fieldDescription,
  fieldGroup,
  fieldLabel,
  fieldTitle,
} from '../../generated/registry/ui/fieldset'

import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  ToggledCheckbox: { isChecked: S.Boolean },
  ToggledCheckboxWithDescription: { isChecked: S.Boolean },
  ToggledCheckboxNotifications: { isChecked: S.Boolean },
})

export const checkboxView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  h.div(
    [h.Class('flex w-full flex-col gap-8')],
    [
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Basic']),
          field<AppMessage>(
            { orientation: 'horizontal' },
            [
              checkbox<AppMessage>(
                {
                  id: 'terms',
                  label: 'Accept terms and conditions',
                  isChecked: model.isCheckboxChecked,
                  onToggle: (isChecked) => Message.ToggledCheckbox({ isChecked }),
                },
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Description']),
          field<AppMessage>(
            { orientation: 'horizontal' },
            [
              checkbox<AppMessage>(
                {
                  id: 'terms-2',
                  label: 'Accept terms and conditions',
                  description: 'By clicking this checkbox, you agree to the terms and conditions.',
                  isChecked: model.isCheckboxWithDescriptionChecked,
                  onToggle: (isChecked) => Message.ToggledCheckboxWithDescription({ isChecked }),
                },
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Invalid']),
          field<AppMessage>(
            { orientation: 'horizontal', isInvalid: true },
            [
              checkbox<AppMessage>(
                {
                  id: 'terms-3',
                  label: 'Accept terms and conditions',
                  isChecked: false,
                  onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                  className: 'aria-invalid:border-destructive',
                },
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Disabled']),
          field<AppMessage>(
            { orientation: 'horizontal' },
            [
              checkbox<AppMessage>(
                {
                  id: 'toggle-disabled',
                  label: 'Enable notifications',
                  isChecked: false,
                  isDisabled: true,
                  onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                },
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['With Title']),
          fieldGroup<AppMessage>(
            {},
            [
              field<AppMessage>(
                {},
                [
                  fieldLabel<AppMessage>(
                    { for: 'toggle-2' },
                    [
                      field<AppMessage>(
                        { orientation: 'horizontal' },
                        [
                          checkbox<AppMessage>(
                            {
                              id: 'toggle-2',
                              label: '',
                              isChecked: model.isCheckboxNotificationsChecked,
                              onToggle: (isChecked) =>
                                Message.ToggledCheckboxNotifications({ isChecked }),
                            },
                            h,
                          ),
                          fieldContent<AppMessage>(
                            {},
                            [
                              fieldTitle<AppMessage>({}, ['Enable notifications'], h),
                              fieldDescription<AppMessage>(
                                {},
                                ['You can enable or disable notifications at any time.'],
                                h,
                              ),
                            ],
                            h,
                          ),
                        ],
                        h,
                      ),
                    ],
                    h,
                  ),
                ],
                h,
              ),
            ],
            h,
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Group']),
          field<AppMessage>(
            {},
            [
              fieldLabel<AppMessage>({}, ['Show these items on the desktop:'], h),
              field<AppMessage>(
                { orientation: 'horizontal' },
                [
                  checkbox<AppMessage>(
                    {
                      id: 'finder-pref-hard',
                      label: 'Hard disks',
                      isChecked: false,
                      onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                    },
                    h,
                  ),
                ],
                h,
              ),
              field<AppMessage>(
                { orientation: 'horizontal' },
                [
                  checkbox<AppMessage>(
                    {
                      id: 'finder-pref-external',
                      label: 'External disks',
                      isChecked: false,
                      onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                    },
                    h,
                  ),
                ],
                h,
              ),
              field<AppMessage>(
                { orientation: 'horizontal' },
                [
                  checkbox<AppMessage>(
                    {
                      id: 'finder-pref-cds',
                      label: 'CDs, DVDs, and iPods',
                      isChecked: false,
                      onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                    },
                    h,
                  ),
                ],
                h,
              ),
              field<AppMessage>(
                { orientation: 'horizontal' },
                [
                  checkbox<AppMessage>(
                    {
                      id: 'finder-pref-servers',
                      label: 'Connected servers',
                      isChecked: false,
                      onToggle: () => Message.ToggledCheckbox({ isChecked: false }),
                    },
                    h,
                  ),
                ],
                h,
              ),
            ],
            h,
          ),
        ],
      ),
    ],
  )

const fields = {
  isCheckboxChecked: S.Boolean,
  isCheckboxWithDescriptionChecked: S.Boolean,
  isCheckboxNotificationsChecked: S.Boolean,
}

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: {
    isCheckboxChecked: false,
    isCheckboxWithDescriptionChecked: true,
    isCheckboxNotificationsChecked: false,
  },
  messages: [
    Message.ToggledCheckbox,
    Message.ToggledCheckboxWithDescription,
    Message.ToggledCheckboxNotifications,
  ],
  handlers: (model: State) => ({
    ToggledCheckbox: ({ isChecked }: typeof Message.ToggledCheckbox.Type): UpdateReturn => ({
      model: evo(model, { isCheckboxChecked: () => isChecked }),
    }),
    ToggledCheckboxWithDescription: ({
      isChecked,
    }: typeof Message.ToggledCheckboxWithDescription.Type): UpdateReturn => ({
      model: evo(model, { isCheckboxWithDescriptionChecked: () => isChecked }),
    }),
    ToggledCheckboxNotifications: ({
      isChecked,
    }: typeof Message.ToggledCheckboxNotifications.Type): UpdateReturn => ({
      model: evo(model, { isCheckboxNotificationsChecked: () => isChecked }),
    }),
  }),
  samples: [Message.ToggledCheckbox({ isChecked: true })],
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/checkbox

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/checkbox.ts109 lines
import { Checkbox as FoldkitCheckbox } from '@foldkit/ui'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { icon } from '@/lib/icons'
import { Check, Minus } from 'lucide'
import { cn } from '@/lib/utils'

/**
 * foldkit delta (inlined at style resolution): foldkit emits
 * aria-disabled/data-disabled instead of native disabled, and data-checked /
 * data-indeterminate for state.
 *
 */

/** Upstream checkbox component string. The disabled: variants are inert under
 *  foldkit (never native disabled) — compat twins are inlined at style resolution. */
export const checkboxClass =
  'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary aria-disabled:cursor-not-allowed aria-disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50'

export const checkboxIndicatorClass =
  '[&>svg]:size-3.5 grid place-content-center text-current transition-none'

export type CheckboxConfig<M> = Readonly<{
  id: string
  isChecked: boolean
  onToggle: (isChecked: boolean) => M
  label: string
  description?: string
  isDisabled?: boolean
  isReadOnly?: boolean
  isIndeterminate?: boolean
  name?: string
  value?: string
  className?: string
  labelClass?: string
  descriptionClass?: string
  wrapperClass?: string
}>

/** Styled checkbox with label and optional description, built on the
 *  @foldkit/ui Checkbox helper. */
export const checkbox = <M>(config: CheckboxConfig<M>, h: HtmlBuilder<M>): Html =>
  FoldkitCheckbox.view<M>(
    {
      id: config.id,
      isChecked: config.isChecked,
      onToggle: config.onToggle,
      isDisabled: config.isDisabled,
      isReadOnly: config.isReadOnly,
      isIndeterminate: config.isIndeterminate,
      name: config.name,
      value: config.value,
      toView: (attributes) =>
        h.div(
          [h.Class(cn('flex flex-col gap-1', config.wrapperClass))],
          [
            h.div(
              [h.Class('flex items-center gap-2')],
              [
                h.button(
                  [
                    ...attributes.checkbox,
                    h.DataAttribute('slot', 'checkbox'),
                    h.Class(cn(checkboxClass, config.className)),
                  ],
                  config.isChecked || config.isIndeterminate === true
                    ? [
                        h.span(
                          [
                            h.DataAttribute('slot', 'checkbox-indicator'),
                            h.Class(checkboxIndicatorClass),
                          ],
                          [icon(h, config.isIndeterminate === true ? Minus : Check, 'size-3.5')],
                        ),
                      ]
                    : [],
                ),
                ...(attributes.hiddenInput.length > 0
                  ? [h.input([...attributes.hiddenInput])]
                  : []),
                h.label(
                  [
                    ...attributes.label,
                    h.Class(
                      cn(
                        'text-sm font-medium leading-none peer-aria-disabled:cursor-not-allowed peer-aria-disabled:opacity-70 peer-data-[disabled]:opacity-70',
                        config.labelClass,
                      ),
                    ),
                  ],
                  [config.label],
                ),
              ],
            ),
            config.description === undefined
              ? h.empty
              : h.p(
                  [
                    ...attributes.description,
                    h.Class(cn('text-sm text-muted-foreground', config.descriptionClass)),
                  ],
                  [config.description],
                ),
          ],
        ),
    },
    h,
  )