Button
Triggers an action or event, such as submitting a form or opening a dialog.
Installation
Included in poetry-ui — available as
poetry_button
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 button
Default
Open Default standalone
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button { "Default" } %>
<%= poetry_button(variant: :secondary) { "Secondary" } %>
<%= poetry_button(variant: :outline) { "Outline" } %>
<%= poetry_button(variant: :ghost) { "Ghost" } %>
<%= poetry_button(variant: :destructive) { "Destructive" } %>
<%= poetry_button(variant: :link) { "Link" } %>
</div>
As link
Open As link standalone
<%# href: renders a real anchor wearing full button styling - the Rails
equivalent of upstream's buttonVariants-on-<a>. Navigation stays
navigation (no onclick handlers), and every variant works. %>
<div class="flex items-center gap-2">
<%= poetry_button(href: "#as-link") { "Login" } %>
<%= poetry_button(href: "/charts/area", variant: :outline) { "Go to charts" } %>
</div>
Button group
Open Button group standalone
<%# Adjacent actions join into ONE visual control via poetry_button_group -
borders collapse between neighbors. The full surface (separators, text,
split buttons) lives on the Button Group page. %>
<%= poetry_button_group("aria-label": "Message actions") do %>
<%= poetry_button(variant: :outline) { "Archive" } %>
<%= poetry_button(variant: :outline) { "Report" } %>
<%= poetry_button(variant: :outline) { "Snooze" } %>
<% end %>
Icon buttons
Open Icon buttons standalone
<%# The icon-button family: any size: :icon* squares the button and
REQUIRES label: (the accessible name; the icon itself stays
decorative). Every variant wears it, and the dedicated icon-xs ->
icon-lg ladder parallels the text sizes. %>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(size: :icon, label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<%= poetry_button(variant: :secondary, size: :icon, label: "Settings") do %>
<%= poetry_icon(name: :settings) %>
<% end %>
<%= poetry_button(variant: :outline, size: :icon, label: "Share") do %>
<%= poetry_icon(name: :share) %>
<% end %>
<%= poetry_button(variant: :ghost, size: :icon, label: "More") do %>
<%= poetry_icon(name: :ellipsis) %>
<% end %>
<%= poetry_button(variant: :destructive, size: :icon, label: "Delete") do %>
<%= poetry_icon(name: :trash) %>
<% end %>
<%= poetry_button(variant: :link, size: :icon, label: "Open link") do %>
<%= poetry_icon(name: :"external-link") %>
<% end %>
</div>
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(variant: :outline, size: :"icon-xs", label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<%= poetry_button(variant: :outline, size: :"icon-sm", label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<%= poetry_button(variant: :outline, size: :icon, label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<%= poetry_button(variant: :outline, size: :"icon-lg", label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
</div>
</div>
Link
Open Link standalone
<%# The link variant: button semantics (a real <button> for an action),
link clothing - text-primary, underline on hover. For NAVIGATION use
href: (see As link) or poetry_link; this variant is for actions that
should read quietly. %>
<%= poetry_button(variant: :link) { "Link" } %>
Loading
Open Loading standalone
<%# loading: swaps the leading icon for the spinner and sets aria-busy +
disabled with zero JS - never a hand-rolled spinner or a manual
disabled. The content block stays the resting label. %>
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(loading: true) { "Saving changes" } %>
<%= poetry_button(variant: :secondary, loading: true) { "Uploading" } %>
<%= poetry_button(variant: :outline, loading: true) { "Please wait" } %>
</div>
Rounded
Open Rounded standalone
<%# A pill button is a class: override on the root - the radius token is the
only thing that changes, so variant and size still drive everything else.
User classes are appended to the component's own, never replacing them. %>
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(class: "rounded-full") { "Get started" } %>
<%= poetry_button(variant: :outline, class: "rounded-full") { "Learn more" } %>
<%= poetry_button(size: :icon, class: "rounded-full", label: "Add item") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
</div>
Sizes
Open Sizes standalone
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(size: :sm) { "Small" } %>
<%= poetry_button { "Default" } %>
<%= poetry_button(size: :lg) { "Large" } %>
<%= poetry_button(size: :icon, label: "Settings") do %>
<%= poetry_icon(name: :settings) %>
<% end %>
</div>
With icon
Open With icon standalone
<%# Icons ride in the leading/trailing slots (with_leading / with_trailing) -
the visible label stays the content block. The icon slot sizes and
centers whatever it holds, so poetry_icon needs no extra classes. %>
<div class="flex flex-wrap items-center gap-2">
<%= poetry_button(variant: :secondary) do |button| %>
<% button.with_leading { poetry_icon(name: :download) } %>
Export report
<% end %>
<%= poetry_button do |button| %>
<% button.with_trailing { poetry_icon(name: :"arrow-right") } %>
Continue
<% end %>
<%= poetry_button(variant: :destructive) do |button| %>
<% button.with_leading { poetry_icon(name: :"trash-2") } %>
Delete file
<% end %>
</div>
API
Poetry::Ui::Button::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, xs, sm, lg, icon, icon-xs, icon-sm, icon-lg; defaults to :default |
The size axis; the icon* sizes are square icon-only forms (label: required). |
| variant: | Symbol | one of: default, destructive, outline, secondary, ghost, link; defaults to :default |
The visual intent axis; :destructive marks irreversible actions. |
| disabled: | Boolean | defaults to false |
Disables the control (native disabled; aria-disabled on the anchor form). |
| href: | String | The link target; implies the anchor form. | |
| label: | String | The accessible name for icon-only usage - not visible text. | |
| loading: | Boolean | defaults to false |
The no-JS loading state: aria-busy, a spinner, and the control disabled. |
| tag: | Symbol | one of: button, a; defaults to :button |
Renders the same styling on an <a> when :a - navigation wearing button clothes. |
| type: | Symbol | one of: button, submit, reset; defaults to :button |
The native button type; ignored when the button renders as an anchor. |
Slots
| Writer | Description |
|---|---|
| with_leading | Optional leading visual, rendered inside the icon span. |
| with_trailing | Optional trailing visual, rendered inside the icon span. |
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=button] | The rendered control itself (<button>, or <a> when tag: :a) - every visual state rides here |
| [data-slot=icon] | Wrapper span around leading/trailing slot content - sizes and centers whatever it holds |
| [data-slot=label] | The content block's span (display: contents - children join the root's flex row directly) |
| [data-slot=spinner] | The loading indicator, swapped in for the leading icon while loading: |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| button | data-variant | always - the resolved variant | default · destructive · outline · secondary · ghost · link |
| button | data-size | always - the resolved size | default · xs · sm · lg · icon · icon-xs · icon-sm · icon-lg |
| button | data-loading | loading: is set (aria-busy rides along) | — |