Skip to content

Checkbox & radio

Craft doesn't wrap these in a row element. The <input> and its <label> are siblings, and the control you see is drawn on the label's ::before while the native input sits invisibly behind it. That's why the label isn't optional — without it there's nothing to render.

Checkboxes

{{ forms.checkboxGroupField({
    label: "Options",
    name: "options",
    values: ["comments", "notify"],
    options: [
        { label: "Enable comments", value: "comments" },
        { label: "Allow guest authors", value: "guests" },
        { label: "Send notifications", value: "notify" },
    ],
}) }}
<div id="checkbox-group-1" class="checkbox-group">
  <input type="hidden" name="options" value="">
  <input type="checkbox" id="options-comments" class="checkbox" name="options[]" value="comments" checked>
  <label for="options-comments">Enable comments</label>
  <input type="checkbox" id="options-guests" class="checkbox" name="options[]" value="guests">
  <label for="options-guests">Allow guest authors</label>
</div>

checkboxGroup wraps the set in .checkbox-group — a flex column — and posts name[]. Note the hidden input carrying the bare name first: without it, unchecking everything would leave the key absent from the POST entirely, and the old value would survive the save. A lone forms.checkbox emits the same hidden input whenever its name doesn't already end in [].

checked is a real attribute on the input, not a class.

Radios

{{ forms.radioGroupField({
    label: "Status",
    name: "status",
    value: "draft",
    options: [
        { label: "Draft", value: "draft" },
        { label: "Pending review", value: "pending" },
        { label: "Published", value: "live" },
    ],
}) }}

Same shape, .radio-group instead. The dot is the label's ::after, sized --radio-size - 8px and filled --gray-900 when checked.

Toggling other fields

Both accept a toggle, which is how Craft's own settings screens reveal dependent fields with no custom JS — but the two macros wire it up differently.

A checkbox takes the target's id directly. The macro adds .fieldtoggle to the input and sets data-target:

{{ forms.checkboxField({
    label: "Limit relations",
    name: "useLimit",
    checked: settings.useLimit,
    toggle: "limit-settings",
}) }}

<div id="limit-settings" class="{{ settings.useLimit ? '' : 'hidden' }}">
  {{ forms.textField({ label: "Max relations", name: "maxRelations" }) }}
</div>

A radioGroup (and select) instead takes toggle: true plus a targetPrefix, and puts .fieldtoggle and data-target-prefix on the container — each option then reveals #prefix + its own value:

{{ forms.radioGroupField({
    label: "Source",
    name: "source",
    value: "url",
    toggle: true,
    targetPrefix: "source-",
    options: [
        { label: "URL", value: "url" },
        { label: "Upload", value: "upload" },
    ],
}) }}

<div id="source-url">…</div>
<div id="source-upload" class="hidden">…</div>

reverseToggle on a checkbox inverts the logic — the target shows when unchecked.

Note

The tick is an icon font. Craft draws the checkmark with content: "check", a ligature from its own icon font, layered on the same pale box — it does not invert the box to a solid colour the way most UI kits do. The preview above approximates the mark with CSS, so its shape is a stand-in even though the structure, box, and sizing are exact.