Skip to main content

CheckboxGroup

Collect multiple choices as one array-valued field.

Use CheckboxGroup for several choices submitted as one array. Use individual Checkbox controls when the fields are independent.

Basic usage#

import { CheckboxGroup } from 'moraine'

export function Example() {
  return (
    <CheckboxGroup
      aria-label="Preferences"
      items={[
        { value: 'email', label: 'Email' },
        { value: 'sms', label: 'SMS' },
      ]}
    />
  )
}

Playground#

Notification channels

Email alerts

Weekly digests and critical updates

Push notifications

Instant alerts on mobile and desktop

SMS updates

Security and login codes

Props
Slots

Anatomy#

CheckboxGroup [component; slot=root; <div>]
└── fieldset [slot]
    ├── legend [slot]
    └── item [slot]
        ├── container [slot]
        │   ├── input [internal]
        │   └── control [slot]
        │       └── indicator [slot]
        │           └── icon [slot]
        └── wrapper [slot]
            ├── label [slot]
            └── description [slot]

The nested <fieldset> owns group semantics and the optional <legend> association.

Usage#

Selection model#

The group value is an array of item values. Pair value with onValueChange for controlled selection, or use a default value when the group owns its initial state. A disabled group disables all items; an item can also be disabled independently.

import { CheckboxGroup } from 'moraine'
import { createSignal } from 'solid-js'

const NOTIFICATION_OPTIONS = [
  {
    label: 'Email alerts',
    value: 'email',
    description: 'Immediate notifications for critical events.',
  },
  {
    label: 'Weekly digest',
    value: 'digest',
    description: 'Summary of team activity and metric trends.',
  },
  { label: 'SMS updates', value: 'sms', description: 'Urgent security and billing alerts only.' },
]

export function SelectionModel() {
  const [selected, setSelected] = createSignal<string[]>(['email', 'digest'])

  return (
    <div class="flex flex-col gap-3">
      <CheckboxGroup
        legend="Notification preferences"
        items={NOTIFICATION_OPTIONS}
        value={selected()}
        onValueChange={setSelected}
      />
      <p class="text-xs text-muted-foreground">
        Selected values: <span class="text-foreground font-mono">{JSON.stringify(selected())}</span>
      </p>
    </div>
  )
}

Forms#

Give the group a name for native form submission, or place it inside form.Field from createForm for schema binding. Standalone Field supplies labels and messages without validation. Each checkbox keeps its own tab stop; the group does not use radio-style arrow navigation.

import { CheckboxGroup } from 'moraine'

const ROLE_OPTIONS = [
  { label: 'Read repository', value: 'read' },
  { label: 'Write changes', value: 'write' },
  { label: 'Admin access', value: 'admin', disabled: true },
]

export function FormBehavior() {
  return (
    <div class="max-w-md">
      <CheckboxGroup name="permissions" items={ROLE_OPTIONS} defaultValue={['read']} />
    </div>
  )
}

Examples#

Controlled value and disabled items#

import { CheckboxGroup } from 'moraine'
import { createSignal } from 'solid-js'

export function ControlledDisabledItems() {
  const ROLES = [
    { value: 'viewer', label: 'Viewer', description: 'Read-only access to repositories' },
    { value: 'developer', label: 'Developer', description: 'Can push commits and create PRs' },
    { value: 'maintainer', label: 'Maintainer', description: 'Can merge PRs and manage releases' },
    {
      value: 'owner',
      label: 'Owner (Immutable)',
      description: 'Primary organization administrator',
      disabled: true,
    },
  ]

  const [value, setValue] = createSignal<string[]>(['developer', 'owner'])

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-xl space-y-4">
      <CheckboxGroup
        legend="Team member role permissions"
        variant="card"
        items={ROLES}
        value={value()}
        onValueChange={setValue}
      />
      <p class="text-xs text-muted-foreground">
        Active roles: <span class="text-foreground font-medium">{value().join(', ')}</span>
      </p>
    </div>
  )
}

Custom indicator#

import { CheckboxGroup } from 'moraine'
import { For } from 'solid-js'

const NOTIFICATIONS = [
  {
    value: 'mentions',
    label: 'Direct @mentions',
    description: 'When someone mentions you in a thread',
  },
  { value: 'assignee', label: 'Issue assigned', description: 'When an issue is assigned to you' },
  {
    value: 'review',
    label: 'Review requested',
    description: 'When your review is required on a PR',
  },
]

const INDICATORS = ['start', 'end', 'hidden'] as const

export function Indicator() {
  return (
    <div class="gap-4 grid md:grid-cols-2 xl:grid-cols-3">
      <For each={INDICATORS}>
        {(indicator) => (
          <div class="p-4 b-1 b-border rounded-xl space-y-2">
            <p class="text-xs text-muted-foreground tracking-wider font-semibold uppercase">
              Indicator: {indicator}
            </p>
            <CheckboxGroup
              legend="Activity alerts"
              items={NOTIFICATIONS}
              indicator={indicator}
              defaultValue={['mentions', 'review']}
            />
          </div>
        )}
      </For>
    </div>
  )
}

Form integration#

import { Button, CheckboxGroup, createForm } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

const ROLE_OPTIONS = [
  { label: 'Read repository', value: 'read' },
  { label: 'Write changes', value: 'write' },
  { label: 'Admin access', value: 'admin' },
]

export function FormIntegration() {
  const [submitted, setSubmitted] = createSignal<string[]>([])
  const form = createForm({
    schema: v.object({
      permissions: v.pipe(
        v.array(v.string()),
        v.minLength(1, 'Select at least one permission role.'),
      ),
    }),
    initialInput: { permissions: ['read'] },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmitted(output.permissions)}>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="permissions"
          label="Role permissions"
          description="Assigned permissions for team members."
          required
        >
          <CheckboxGroup items={ROLE_OPTIONS} />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">
            Active roles: {submitted().length ? submitted().join(', ') : 'read'}
          </p>
        </div>
      </div>
    </form.Form>
  )
}

Attributes#

Attributes
data-disabledSlot: checkbox-group, checkbox-group-item, checkbox-control, checkbox-indicatorDescription: Present when the component, slot, or item is disabled.
data-invalidSlot: checkbox-group, checkbox-group-item, checkbox-controlDescription: Present when the field or form has a validation error.
data-readonlySlot: checkbox-group, checkbox-group-item, checkbox-controlDescription: Present when the field is in read-only mode.
data-requiredSlot: checkbox-group, checkbox-group-legend, checkbox-group-item, checkbox-control, checkbox-labelDescription: Present when the field input is required.
data-checkedSlot: checkbox-group-item, checkbox-control, checkbox-indicatorDescription: Present when the item or toggle is checked.
data-indeterminateSlot: checkbox-group-item, checkbox-control, checkbox-indicatorDescription: Present when the checkbox or progress state is indeterminate.
data-uncheckedSlot: checkbox-group-item, checkbox-controlDescription: Present when the checkbox, radio item, or switch is unchecked.

Props#

Props for the CheckboxGroup component. Renders a <div> element by default.

Prop