Button Group
Visually joins adjacent buttons and controls into one group.
Installation
Included in poetry-ui — available as
poetry_button_group
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-group
Default
Open Default standalone
<%= poetry_button_group("aria-label": "Text alignment") do %>
<%= poetry_button(variant: :outline) { "Left" } %>
<%= poetry_button(variant: :outline) { "Center" } %>
<%= poetry_button(variant: :outline) { "Right" } %>
<% end %>
Input action
Open Input action standalone
<%= poetry_button_group("aria-label": "Search library") do %>
<%= poetry_input(name: "query", type: "search", placeholder: "Search records") %>
<%= poetry_button(variant: :outline, size: :icon, label: "Search") do %>
<%= poetry_icon(name: :search) %>
<% end %>
<% end %>
Input group
Open Input group standalone
<%# The pill composer: [--radius:9999rem] re-derives the whole radius
scale, so the nested composition rounds as one capsule family. The
mic is a real control (poetry_input_group_button) - wire it with
your own data-action. %>
<%= poetry_button_group("aria-label": "Voice composer", class: "[--radius:9999rem]") do %>
<%= poetry_button_group do %>
<%= poetry_button(variant: :outline, size: :icon, label: "Add attachment") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<% end %>
<%= poetry_button_group(class: "w-64") do %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "voice_message", placeholder: "Send a message…") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_button(size: :"icon-xs", label: "Voice mode") do %>
<%= poetry_icon(name: :"audio-lines") %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Nested
Open Nested standalone
<%# Groups nest: an outer group spaces INNER groups apart (gap, not
joining), and each inner group keeps its own segmented corners.
The input-group is a full member - the chains square it directly. %>
<%= poetry_button_group("aria-label": "Composer") do %>
<%= poetry_button_group do %>
<%= poetry_button(variant: :outline, size: :icon, label: "Add attachment") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<% end %>
<%= poetry_button_group(class: "w-64") do %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "message", placeholder: "Send a message…") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_icon(name: :"audio-lines") %>
<% end %>
<% end %>
<% end %>
<% end %>
Popover
Open Popover standalone
<%# The popover's wrapper is a full group member, like select and
dropdown - square joints, rounded outer edge on its trigger. %>
<div class="pb-40">
<%= poetry_button_group("aria-label": "Copilot") do %>
<%= poetry_button(variant: :outline) do %>
<%= poetry_icon(name: :bot) %>
Copilot
<% end %>
<%= poetry_popover(content_class: "w-72") do |popover| %>
<% popover.with_trigger(variant: :outline, size: :icon, label: "Open Copilot options") do %>
<%= poetry_icon(name: :"chevron-down") %>
<% end %>
<% popover.with_title { "Start a new task with Copilot" } %>
<% popover.with_description { "Describe your task in natural language." } %>
<% end %>
<% end %>
</div>
Separator
Open Separator standalone
<%# Separators pair with BORDERLESS variants: on secondary members the
1px line is the only divide. Outline members already divide via
their own borders - a separator there doubles the line and reads
heavy. %>
<%= poetry_button_group("aria-label": "Clipboard") do %>
<%= poetry_button(variant: :secondary, size: :sm) { "Copy" } %>
<%= poetry_button_group_separator %>
<%= poetry_button(variant: :secondary, size: :sm) { "Paste" } %>
<% end %>
Sizes
Open Sizes standalone
<div class="flex flex-col items-start gap-4">
<%= poetry_button_group("aria-label": "Small toolbar") do %>
<%= poetry_button(variant: :outline, size: :sm) { "Cut" } %>
<%= poetry_button(variant: :outline, size: :sm) { "Copy" } %>
<%= poetry_button(variant: :outline, size: :sm) { "Paste" } %>
<% end %>
<%= poetry_button_group("aria-label": "Default toolbar") do %>
<%= poetry_button(variant: :outline) { "Cut" } %>
<%= poetry_button(variant: :outline) { "Copy" } %>
<%= poetry_button(variant: :outline) { "Paste" } %>
<% end %>
<%= poetry_button_group("aria-label": "Large toolbar") do %>
<%= poetry_button(variant: :outline, size: :lg) { "Cut" } %>
<%= poetry_button(variant: :outline, size: :lg) { "Copy" } %>
<%= poetry_button(variant: :outline, size: :lg) { "Paste" } %>
<% end %>
</div>
Split
Open Split standalone
<%# The split pair on a borderless variant: the separator IS the divide
(secondary buttons carry no borders of their own). %>
<%= poetry_button_group("aria-label": "Add item") do %>
<%= poetry_button(variant: :secondary) { "Button" } %>
<%= poetry_button_group_separator %>
<%= poetry_button(variant: :secondary, size: :icon, label: "Add") do %>
<%= poetry_icon(name: :plus) %>
<% end %>
<% end %>
Text prefix
Open Text prefix standalone
<%= poetry_button_group("aria-label": "Site address") do %>
<%= poetry_button_group_text { "https://" } %>
<%= poetry_input(name: "domain", placeholder: "example.com") %>
<% end %>
Vertical
Open Vertical standalone
<%= poetry_button_group(orientation: :vertical, "aria-label": "Vertical actions") do %>
<%= poetry_button(variant: :outline) { "Top" } %>
<%= poetry_button(variant: :outline) { "Bottom" } %>
<% end %>
With dropdown
Open With dropdown standalone
<%# A split action: the labeled button acts, the joined chevron opens the
menu. The dropdown's wrapper is a full group member - the theme chains
reach its trigger for the corner math. %>
<%= poetry_button_group("aria-label": "Follow actions") do %>
<%= poetry_button(variant: :outline) { "Follow" } %>
<%= poetry_dropdown_menu(align: :end) do |menu| %>
<% menu.with_trigger(variant: :outline, size: :icon, label: "More follow options") do %>
<%= poetry_icon(name: :"chevron-down") %>
<% end %>
<% menu.with_group do |group| %>
<% group.with_item do %><%= poetry_icon(name: :"volume-off") %>Mute conversation<% end %>
<% group.with_item do %><%= poetry_icon(name: :check) %>Mark as read<% end %>
<% group.with_item do %><%= poetry_icon(name: :share) %>Share thread<% end %>
<% end %>
<% menu.with_separator %>
<% menu.with_item(variant: :destructive) do %><%= poetry_icon(name: :"user-round-x") %>Block user<% end %>
<% end %>
<% end %>
With select
Open With select standalone
<%= poetry_button_group("aria-label": "Currency amount") do %>
<%= poetry_select(placeholder: "USD", "aria-label": "Currency") do |select| %>
<% select.with_item(value: "usd") { "USD" } %>
<% select.with_item(value: "eur") { "EUR" } %>
<% select.with_item(value: "gbp") { "GBP" } %>
<% end %>
<%= poetry_input(name: "amount", placeholder: "0.00") %>
<%= poetry_button(variant: :outline) { "Convert" } %>
<% end %>
API
Poetry::Ui::ButtonGroup::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 |
|---|---|---|---|
| orientation: | Symbol | one of: horizontal, vertical; defaults to :horizontal |
The join axis - a horizontal row or a vertical stack. |
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-group] | The role=group root - its selectors join ANY data-slot children into the segmented unit |
| [data-slot=button-group-text] | A non-button member (the poetry_button_group_text helper's div) - a text affix joined like a button |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| button-group | data-orientation | the join axis | horizontal · vertical |