Checkbox
A control for toggling a single value on or off.
Installation
Included in poetry-ui — available as
poetry_checkbox
the moment you've installed Poetry,
with no per-component step. To own the source and edit it, copy it into your app:
bin/rails g poetry:add checkbox
Default
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "terms", id: "checkbox-terms") %>
<%= poetry_label(for_id: "checkbox-terms") { "Accept terms and conditions" } %>
</div>
Card
<%# The whole card is the label. A <button role=checkbox> is a labelable
element, so wrapping it in poetry_label (for_id pointing at the box)
makes a click anywhere on the card toggle the control. has-[]: variants
read the button's aria-checked to light up the selected border. %>
<%= poetry_label(
for_id: "checkbox-card-backups",
class: "flex max-w-sm items-start gap-3 rounded-lg border bg-card p-4 " \
"cursor-pointer has-[[aria-checked=true]]:border-primary " \
"has-[[aria-checked=true]]:bg-primary/5"
) do %>
<%= poetry_checkbox(name: "automatic_backups", checked: true, id: "checkbox-card-backups") %>
<div class="grid gap-1.5 leading-none">
<span class="text-sm font-medium">Automatic backups</span>
<span class="text-sm text-muted-foreground">
Snapshot your workspace every night and keep the last 30 days.
</span>
</div>
<% end %>
Disabled
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "notifications", disabled: true, id: "checkbox-disabled") %>
<%= poetry_label(for_id: "checkbox-disabled") { "Enable notifications" } %>
</div>
<%# Disabled inputs don't submit - a disabled checked checkbox
submits nothing, not "1". %>
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "plan_locked", checked: true, disabled: true, id: "checkbox-locked") %>
<%= poetry_label(for_id: "checkbox-locked") { "Locked on (submits nothing)" } %>
</div>
</div>
Group
<%# The select-all family: the parent checkbox fans out to every member
and member toggles re-derive it - all checked, none, or the mixed
indeterminate state in between. %>
<%= poetry_checkbox_group(class: "flex flex-col gap-3") do %>
<div class="flex items-center gap-3">
<%= poetry_checkbox_group_all(name: "notifications_all", id: "cbg-all") %>
<%= poetry_label(for_id: "cbg-all") { "All notifications" } %>
</div>
<div class="ml-6 flex flex-col gap-3">
<div class="flex items-center gap-3">
<%= poetry_checkbox_group_item(name: "notify_mentions", id: "cbg-mentions", checked: true) %>
<%= poetry_label(for_id: "cbg-mentions") { "Mentions" } %>
</div>
<div class="flex items-center gap-3">
<%= poetry_checkbox_group_item(name: "notify_replies", id: "cbg-replies") %>
<%= poetry_label(for_id: "cbg-replies") { "Replies" } %>
</div>
<div class="flex items-center gap-3">
<%= poetry_checkbox_group_item(name: "notify_digests", id: "cbg-digests") %>
<%= poetry_label(for_id: "cbg-digests") { "Weekly digest" } %>
</div>
</div>
<% end %>
In a field
<%# Field-bound: control_attributes lands the id (the label-for target),
aria-describedby, and aria-required on the checkbox button. %>
<% field = Poetry::Ui::Field::Component.new(
id: "checkbox-newsletter", label_text: "Email newsletter",
hint: "Sent weekly. Unsubscribe anytime.", required: true,
orientation: :horizontal
) %>
<div class="w-80">
<%= render field do %>
<%= poetry_checkbox(name: "newsletter", checked: true,
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
In a table
| Invoice | Amount | |
|---|---|---|
| INV-1001 | $1,250.00 | |
| INV-1002 | $340.00 | |
| INV-1003 | $780.00 |
<%# Row selection inside a table. These boxes drive UI state, not a form,
so they run in visual-only mode (no name:) - the documented use for the
nameless checkbox. The wrapper wires the select-all recipe
(poetry--core--checkbox-group): the header box (target: all) fans out
to the rows (target: item), and row toggles re-derive it - all rows
checked, none, or the :indeterminate in-between. Each box gets an
accessible name from label:, since there is no visible text beside
it. %>
<div class="w-full max-w-md rounded-lg border"
data-controller="poetry--core--checkbox-group"
data-action="poetry:checkbox:change->poetry--core--checkbox-group#changed">
<table class="w-full text-sm">
<thead>
<tr class="border-b">
<th class="w-10 p-3 text-left">
<%= poetry_checkbox(checked: :indeterminate, label: "Select all invoices",
data: { "poetry--core--checkbox-group-target": "all" }) %>
</th>
<th class="p-3 text-left font-medium text-muted-foreground">Invoice</th>
<th class="p-3 text-right font-medium text-muted-foreground">Amount</th>
</tr>
</thead>
<tbody>
<tr class="border-b">
<td class="p-3"><%= poetry_checkbox(checked: true, label: "Select INV-1001",
data: { "poetry--core--checkbox-group-target": "item" }) %></td>
<td class="p-3">INV-1001</td>
<td class="p-3 text-right">$1,250.00</td>
</tr>
<tr class="border-b">
<td class="p-3"><%= poetry_checkbox(label: "Select INV-1002",
data: { "poetry--core--checkbox-group-target": "item" }) %></td>
<td class="p-3">INV-1002</td>
<td class="p-3 text-right">$340.00</td>
</tr>
<tr>
<td class="p-3"><%= poetry_checkbox(checked: true, label: "Select INV-1003",
data: { "poetry--core--checkbox-group-target": "item" }) %></td>
<td class="p-3">INV-1003</td>
<td class="p-3 text-right">$780.00</td>
</tr>
</tbody>
</table>
</div>
States
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "newsletter", checked: true, id: "checkbox-checked") %>
<%= poetry_label(for_id: "checkbox-checked") { "Checked" } %>
</div>
<%# Indeterminate is server/programmatic only - the select-all parent
over a partial selection. Announces as "mixed"; the first toggle
resolves to checked. %>
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "select_all", checked: :indeterminate, id: "checkbox-indeterminate") %>
<%= poetry_label(for_id: "checkbox-indeterminate") { "Select all rows" } %>
</div>
<div class="flex items-center gap-3">
<%= poetry_checkbox(name: "terms_invalid", "aria-invalid": true, id: "checkbox-invalid") %>
<%= poetry_label(for_id: "checkbox-invalid") { "Accept terms and conditions" } %>
</div>
</div>
With description
Diagnostics help us find and fix crashes faster. No personal information is ever collected.
<%# A checkbox with a primary label and a secondary line of description
text. Top-aligned (items-start) so the box lines up with the first line
of copy rather than centering against the whole block. %>
<div class="flex items-start gap-3">
<%= poetry_checkbox(name: "usage_data", checked: true, id: "checkbox-usage") %>
<div class="grid gap-1.5 leading-none">
<%= poetry_label(for_id: "checkbox-usage") { "Share anonymous usage data" } %>
<p class="text-sm text-muted-foreground">
Diagnostics help us find and fix crashes faster. No personal information is ever collected.
</p>
</div>
</div>
API
Poetry::Ui::Checkbox::Component — options are
constructor keywords (the poetry_* helper forwards them);
slots are composed inside the block. Generated from the gem's source documentation.
| Option | Type | Details | Description |
|---|---|---|---|
| checked: | Object | defaults to false |
The state as ONE tri-valued option (true, false, or :indeterminate) - there is no separate indeterminate: flag. |
| disabled: | Boolean | defaults to false |
Disables the visual button and the hidden input together. |
| label: | String | aria-label fallback when no <label for>/Field association exists. | |
| name: | String | Form participation: present renders the hidden native input pair; absent leaves the checkbox visual-only (controlled UI). | |
| required: | Boolean | defaults to false |
aria-required ONLY, never native required - native required on the hidden input would make an unfocusable control invalid. |
| unchecked_value: | String | defaults to "0" |
The paired hidden input's value submitted when unchecked; nil suppresses the pair (the checkbox-array idiom). |
| value: | String | defaults to "1" |
The value submitted when checked (the Rails check_box \"1\"). |
Styling
Every part carries a stable data-slot attribute — target
[data-slot=…] from your own CSS to restyle it. State rides
data attributes on the parts below. This contract is verified against rendered DOM in CI.
| Part | Description |
|---|---|
| [data-slot=checkbox] | The visual button[role=checkbox] - reflects the hidden input via aria-checked plus the checked triple |
| [data-slot=checkbox-indicator] | Centering span around the check glyph (minus when indeterminate) - CSS-hidden while unchecked, never unmounted |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| checkbox | data-checked | checked (the controller reflects every toggle here, aria-checked in step) | — |
| checkbox | data-unchecked | unchecked - the indicator goes invisible | — |
| checkbox | data-indeterminate | checked: :indeterminate (server/programmatic only; the first toggle resolves it to checked) | — |
| checkbox-indicator | data-checked | mirrors the control (the controller reflects state on every part wearing the triple) | — |
| checkbox-indicator | data-unchecked | mirrors the control - the indicator is invisible | — |
| checkbox-indicator | data-indeterminate | mirrors the control - the glyph swaps to minus | — |
Wiring
The Stimulus surface each element carries — declared in the component, verified against rendered DOM in CI. Bare actions fire on the element's default event.
| Element | Controller | Wiring |
|---|---|---|
| root | poetry--core--checked | registers · value input_id (if form_participant?) · toggle on click |