Form controls

Checkbox, Switch, RadioGroup, Label, Toggle and ToggleGroup. Every one of these is the real element, which is the reason this page is short: the role, the state, form participation, validation and the keyboard are the browser's, and there is nothing to keep in sync.

import { Checkbox, Switch, RadioGroup, Label, Toggle, ToggleGroup } from '@apostel/bedrock'
Loading demo…
Source
import { useState } from 'react'
import { Checkbox, Label, RadioGroup, Switch, Toggle, ToggleGroup } from '../../src/index'

/**
 * Every control here is the real element: `<input type="checkbox">` for the
 * checkbox and the switch, `<input type="radio">` for the group. Form
 * participation, validation, the space bar and arrow-key roving inside a radio
 * group are all the browser's, so there is nothing to keep in sync.
 */
export default function FormsDemo() {
  const [log, setLog] = useState<string[]>([])
  const push = (entry: string) => setLog((entries) => [entry, ...entries].slice(0, 4))

  return (
    <div style={{ width: '100%' }}>
      <div className="demo-row">
        <Checkbox.Root id="ship" onCheckedChange={(on) => push(`checkbox: ${on}`)} />
        <Label.Root htmlFor="ship">Email me when it ships</Label.Root>
      </div>

      <div className="demo-row">
        <Switch.Root id="beta" onCheckedChange={(on) => push(`switch: ${on}`)} />
        <Label.Root htmlFor="beta">Join the beta</Label.Root>
      </div>

      <RadioGroup.Root defaultValue="standard" onValueChange={(value) => push(`radio: ${value}`)}>
        <div className="demo-row">
          <RadioGroup.Item value="standard" id="standard" />
          <Label.Root htmlFor="standard">Standard</Label.Root>
        </div>
        <div className="demo-row">
          <RadioGroup.Item value="express" id="express" />
          <Label.Root htmlFor="express">Express</Label.Root>
        </div>
      </RadioGroup.Root>

      <div className="demo-row">
        <Toggle.Root onPressedChange={(on) => push(`toggle: ${on}`)}>Pin</Toggle.Root>
        <ToggleGroup.Root type="multiple">
          <ToggleGroup.Item value="bold">Bold</ToggleGroup.Item>
          <ToggleGroup.Item value="italic">Italic</ToggleGroup.Item>
          <ToggleGroup.Item value="underline">Underline</ToggleGroup.Item>
        </ToggleGroup.Root>
      </div>

      <output>{log.join(' · ') || 'no changes yet'}</output>
    </div>
  )
}

Checkbox

Renders <input type="checkbox" data-bedrock-checkbox>.

prop type notes
defaultChecked boolean Plain attribute.
indeterminate boolean Property-only in the DOM, so this is the one effect here.
onCheckedChange (checked: boolean) => void Fires alongside onChange.
asChild boolean Supported.

Radix renders a <button role="checkbox"> beside a hidden input and keeps the two in step. There is nothing here to keep in step.

Checkbox.Indicator renders nothing, and warns in development if you give it a className. An <input> cannot have children. Draw the tick with ::before on the root:

[data-bedrock-checkbox] { appearance: none }
[data-bedrock-checkbox]:checked::before { content: '✓' }
[data-bedrock-checkbox]:indeterminate::before { content: '–' }

Switch

An <input type="checkbox" role="switch">. Same props as Checkbox.

Switch.Thumb renders nothing, for the same reason as Checkbox.Indicator. Draw it with a pseudo-element.

RadioGroup

RadioGroup.Root renders a <div role="radiogroup">; RadioGroup.Item renders <input type="radio"> with a shared name.

prop type on
defaultValue string Root
onValueChange (value: string) => void Root
value string Item (required)

Arrow-key roving between radios in a group is the browser's, not ours — a shared name is all it takes. RadioGroup.Indicator renders nothing.

Label

Renders <label data-bedrock-label>. Takes htmlFor.

Double-clicking a label selects text in some engines, which is never what a label is for; that is suppressed. Everything else — click-to-focus, click-to-toggle, the accessible name — is the element's.

Toggle and ToggleGroup

Toggle.Root renders <button aria-pressed>.

prop type notes
defaultPressed boolean
onPressedChange (pressed: boolean) => void

ToggleGroup.Root takes type="single" | "multiple" and gives its items roving focus — one tab stop for the group, arrow keys within it. That part is written, because HTML has no toggle-group widget.

ToggleGroup.Item takes a required value.

What is not here