Switch
A toggle for turning a setting on or off.
Installation
Included in poetry-ui — available as
poetry_switch
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 switch
Default
<div class="flex items-center gap-3">
<%= poetry_switch(name: "airplane_mode", id: "switch-airplane") %>
<%= poetry_label(for_id: "switch-airplane") { "Airplane mode" } %>
</div>
Card
A Monday summary of everything that changed across your workspace.
<%# Choice card: a bordered container turns the field into a self-contained
settings row. The horizontal Field keeps the switch left with its label
and hint stacked right, and the whole card shares one border - clicking
the label still flips the switch via for=. %>
<div class="w-full max-w-md rounded-lg border bg-card p-4 shadow-sm">
<%= poetry_field(id: "switch-card-digest",
label_text: "Weekly digest",
hint: "A Monday summary of everything that changed across your workspace.",
orientation: :horizontal) do |field| %>
<%= poetry_switch(name: "weekly_digest", checked: true,
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
Description
Focus is shared across devices, and turns off when you leave the app.
<%# The setting row (orientation: :setting): label + description stacked
left, switch right on the label line - upstream's content-first
horizontal Field. control_attributes threads id + aria-describedby. %>
<div class="w-full max-w-sm">
<%= poetry_field(id: "switch-focus-mode", label_text: "Share across devices",
hint: "Focus is shared across devices, and turns off when you leave the app.",
orientation: :setting) do |field| %>
<%= poetry_switch(name: "focus_mode", **field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
In a field
Applied the moment it flips.
<%# Field-bound: control_attributes lands the id (the label-for target)
and aria-describedby on the switch button. %>
<% field = Poetry::Ui::Field::Component.new(
id: "switch-notifications", label_text: "Notifications",
hint: "Applied the moment it flips.", orientation: :horizontal
) %>
<div class="w-80">
<%= render field do %>
<%= poetry_switch(name: "email_notifications", checked: true,
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
Invalid
You must accept the terms and conditions to continue.
<%# Invalid as a setting row (upstream's switch-invalid arrangement):
invalid: flips data-invalid + the switch's aria-invalid while the
requirement copy stays a MUTED hint (upstream keeps FieldDescription
muted - only the label and control wear the destructive skin). %>
<div class="w-full max-w-sm">
<%= poetry_field(id: "switch-terms", label_text: "Accept terms and conditions",
hint: "You must accept the terms and conditions to continue.",
invalid: true, orientation: :setting) do |field| %>
<%= poetry_switch(name: "accept_terms", **field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
Sizes
<%# size: :sm for dense settings lists - the thumb derives its size from
the control's data-size (no per-element size classes). %>
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<%= poetry_switch(name: "notifications", checked: true, id: "switch-size-default") %>
<%= poetry_label(for_id: "switch-size-default") { "Default" } %>
</div>
<div class="flex items-center gap-3">
<%= poetry_switch(name: "compact", size: :sm, checked: true, id: "switch-size-sm") %>
<%= poetry_label(for_id: "switch-size-sm") { "Compact density" } %>
</div>
</div>
States
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<%= poetry_switch(name: "wifi", checked: true, id: "switch-checked") %>
<%= poetry_label(for_id: "switch-checked") { "Wi-Fi" } %>
</div>
<div class="flex items-center gap-3">
<%= poetry_switch(name: "bluetooth", disabled: true, id: "switch-disabled") %>
<%= poetry_label(for_id: "switch-disabled") { "Bluetooth" } %>
</div>
<%# Disabled inputs don't submit - a disabled checked switch
submits nothing, not "1". %>
<div class="flex items-center gap-3">
<%= poetry_switch(name: "managed_policy", checked: true, disabled: true, id: "switch-locked") %>
<%= poetry_label(for_id: "switch-locked") { "Locked on" } %>
</div>
</div>
API
Poetry::Ui::Switch::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 |
|---|---|---|---|
| size: | Symbol | one of: default, sm; defaults to :default |
The control's size axis; the thumb scales to match. |
| checked: | Boolean | defaults to false |
The server-rendered on/off state. |
| disabled: | Boolean | defaults to false |
Disables the control and its hidden input - a disabled switch neither toggles nor submits. |
| label: | String | The accessible name, rendered as aria-label - not visible text. | |
| name: | String | Names the hidden input, making the switch a form participant. | |
| required: | Boolean | defaults to false |
Marks the switch required via aria-required (never the native attribute). |
| unchecked_value: | String | defaults to "0" |
Submitted when the switch is off, so the field always posts. Ignored without name:. |
| value: | String | defaults to "1" |
Submitted when the switch is on. Ignored without name:. |
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=switch] | The visual button[role=switch] - reflects the hidden input via aria-checked plus the checked pair (never indeterminate) |
| [data-slot=switch-thumb] | The sliding knob - travel is pure CSS off the checked pair |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| switch | data-checked | on (the shared checked controller reflects every toggle here, aria-checked in step) | — |
| switch | data-unchecked | off (the server-rendered default) | — |
| switch | data-size | always - the resolved size (the thumb reads it via group/switch selectors) | default · sm |
| switch-thumb | data-checked | mirrors the control (the controller reflects state on every part wearing the pair) | — |
| switch-thumb | data-unchecked | mirrors the control - the thumb sits at the start | — |
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 |