Input Group
One bordered surface combining an input with buttons, icons, or add-ons.
Installation
Included in poetry-ui — available as
poetry_input_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 input-group
Default
Open Default standalone
12 results
<%# Upstream demo: leading icon addon, borderless control, trailing
result count. The group wears the border and focus ring; the control
inside must be poetry_input_group_input (a plain poetry_input keeps
its own chrome and double-borders the group). %>
<div class="w-80">
<%= poetry_label(for_id: "input-group-search", class: "sr-only") { "Search" } %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(id: "input-group-search", name: "q", placeholder: "Search...") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :search) %>
<% end %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
12 results
<% end %>
<% end %>
</div>
Align
Open Align standalone
Icon positioned at the start.
Icon positioned at the end.
Full Name
Header positioned above the input.
USD
Footer positioned below the input.
<%# The four addon axes (upstream Align): inline addons ride the row,
block addons take a full-width row - the group's has-[] chain reflows
it to a column, no h-auto needed. Each field wires its label to the
borderless control inside the group. Textarea + block addons live in
the Textarea block/header examples. %>
<div class="flex w-full max-w-sm flex-col gap-6">
<%= poetry_field(id: "align-inline-start", label_text: "Inline start",
hint: "Icon positioned at the start.") do |field| %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "q", placeholder: "Search...",
**field.control_attributes.transform_keys(&:to_sym)) %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :search, class: "text-muted-foreground") %>
<% end %>
<% end %>
<% end %>
<%= poetry_field(id: "align-inline-end", label_text: "Inline end",
hint: "Icon positioned at the end.") do |field| %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(type: "password", name: "password", placeholder: "Enter password",
**field.control_attributes.transform_keys(&:to_sym)) %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_icon(name: :"eye-off") %>
<% end %>
<% end %>
<% end %>
<%= poetry_field(id: "align-block-start", label_text: "Block start",
hint: "Header positioned above the input.") do |field| %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "full_name", placeholder: "Enter your name",
**field.control_attributes.transform_keys(&:to_sym)) %>
<%= poetry_input_group_addon(align: :"block-start") do %>
<%= poetry_input_group_text { "Full Name" } %>
<% end %>
<% end %>
<% end %>
<%= poetry_field(id: "align-block-end", label_text: "Block end",
hint: "Footer positioned below the input.") do |field| %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "amount", placeholder: "Enter amount",
**field.control_attributes.transform_keys(&:to_sym)) %>
<%= poetry_input_group_addon(align: :"block-end") do %>
<%= poetry_input_group_text { "USD" } %>
<% end %>
<% end %>
<% end %>
</div>
Button
Open Button standalone
https://
Your connection is not secure.
You should not enter any sensitive information on this site.
<%# Tiny in-group actions (upstream Button): a copy affordance, a pill
group whose leading addon opens a Popover, and a labeled search
action. The group's --radius var drives the pill; the popover trigger
rides Button through the with_trigger slot. %>
<div class="grid w-full max-w-sm gap-6">
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "share_url", value: "https://poetry.example/share",
readonly: true, "aria-label": "Share URL") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_button(size: :"icon-xs", label: "Copy") do %>
<%= poetry_icon(name: :copy) %>
<% end %>
<% end %>
<% end %>
<%= poetry_input_group(class: "[--radius:9999px]") do %>
<%= poetry_input_group_addon do %>
<%= poetry_popover(align: :start) do |popover| %>
<% popover.with_trigger(variant: :secondary, size: :"icon-xs", label: "Connection info",
class: "rounded-full") do %>
<%= poetry_icon(name: :info) %>
<% end %>
<div class="flex flex-col gap-1 text-sm">
<p class="font-medium">Your connection is not secure.</p>
<p>You should not enter any sensitive information on this site.</p>
</div>
<% end %>
<%= poetry_input_group_text { "https://" } %>
<% end %>
<%= poetry_input_group_input(name: "secure_url", "aria-label": "URL") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_button(size: :"icon-xs", label: "Favorite") do %>
<%= poetry_icon(name: :star) %>
<% end %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "search", placeholder: "Type to search...",
"aria-label": "Search") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_button(variant: :secondary) { "Search" } %>
<% end %>
<% end %>
</div>
Custom
Open Custom standalone
<%# Bring your own control (upstream Custom Input): any element stamped
data-slot=input-group-control joins the group - the focus ring and
invalid chrome key on the slot, not the tag. Upstream reaches for a
React autosize textarea; CSS field-sizing grows this one natively,
zero JS. %>
<div class="w-full max-w-sm">
<%= poetry_input_group do %>
<%= tag.textarea nil, name: "message", placeholder: "Autoresize textarea...",
"aria-label": "Message", data: { slot: "input-group-control" },
class: "flex field-sizing-content min-h-16 w-full resize-none rounded-md " \
"bg-transparent px-3 py-2.5 text-base outline-none md:text-sm" %>
<%= poetry_input_group_addon(align: :"block-end") do %>
<%= poetry_input_group_button(size: :sm, variant: :default, class: "ml-auto") { "Submit" } %>
<% end %>
<% end %>
</div>
Dropdown
Open Dropdown standalone
<%# Menus in the addon row (upstream Dropdown): the dropdown trigger
rides Button through with_trigger, so it sits in the addon like any
tiny in-group action - an icon trigger first, a labeled one second. %>
<div class="grid w-full max-w-sm gap-4">
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "file_name", placeholder: "Enter file name",
"aria-label": "File name") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_dropdown_menu(align: :end, side_offset: 8, align_offset: -4) do |menu| %>
<% menu.with_trigger(variant: :ghost, size: :"icon-xs", label: "More") do %>
<%= poetry_icon(name: :ellipsis) %>
<% end %>
<% menu.with_group do |group| %>
<% group.with_item { "Settings" } %>
<% group.with_item { "Copy path" } %>
<% group.with_item { "Open location" } %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "query", placeholder: "Enter search query",
"aria-label": "Search query") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_dropdown_menu(align: :end, side_offset: 8, align_offset: -4) do |menu| %>
<% menu.with_trigger(variant: :ghost, size: :xs, class: "text-xs") do %>
Search In... <%= poetry_icon(name: :"chevron-down", class: "size-3") %>
<% end %>
<% menu.with_group do |group| %>
<% group.with_item { "Documentation" } %>
<% group.with_item { "Blog Posts" } %>
<% group.with_item { "Changelog" } %>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
Icon
Open Icon standalone
<%# Icon addons (upstream Icon): leading, trailing, both edges, or
several icons sharing one addon. Icons stay decorative - the inputs
carry their own accessible names. %>
<div class="grid w-full max-w-sm gap-6">
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "q", placeholder: "Search...", "aria-label": "Search") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :search) %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(type: "email", name: "email", placeholder: "Enter your email",
"aria-label": "Email") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :mail) %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "card", placeholder: "Card number", "aria-label": "Card number") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :"credit-card") %>
<% end %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_icon(name: :check) %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "card_alt", placeholder: "Card number", "aria-label": "Card number") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_icon(name: :star) %>
<%= poetry_icon(name: :info) %>
<% end %>
<% end %>
</div>
Kbd
Open Kbd standalone
⌘K
<%# The shortcut hint (upstream Kbd): a leading icon and a trailing
poetry_kbd riding the addon row. %>
<div class="w-full max-w-sm">
<%= poetry_label(for_id: "input-group-kbd-search", class: "sr-only") { "Search" } %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(id: "input-group-kbd-search", name: "q", placeholder: "Search...") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :search, class: "text-muted-foreground") %>
<% end %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_kbd { "⌘K" } %>
<% end %>
<% end %>
</div>
Spinner
Open Spinner standalone
Saving...
Please wait...
<%# Pending states (upstream Spinner): poetry_spinner announces itself
(role=status) wherever it rides; the last row shows the raw
animate-spin icon route for comparison - reach for the Spinner when
the wait should be announced. %>
<div class="grid w-full max-w-sm gap-4">
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "search", placeholder: "Searching...", "aria-label": "Search") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_spinner %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "job", placeholder: "Processing...", "aria-label": "Job name") %>
<%= poetry_input_group_addon do %>
<%= poetry_spinner %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "draft", placeholder: "Saving changes...", "aria-label": "Draft") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_text { "Saving..." } %>
<%= poetry_spinner(label: "Saving") %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "refresh", placeholder: "Refreshing data...",
"aria-label": "Data set") %>
<%= poetry_input_group_addon do %>
<%= poetry_icon(name: :loader, class: "animate-spin") %>
<% end %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_text(class: "text-muted-foreground") { "Please wait..." } %>
<% end %>
<% end %>
</div>
Text
Open Text standalone
$
USD
https://
.com
@company.com
120 characters left
<%# Text addons (upstream Text): prefixes, suffixes, both edges, and a
block-end caption under a textarea. poetry_input_group_text gives the
muted, non-interactive caption treatment. %>
<div class="grid w-full max-w-sm gap-6">
<%= poetry_input_group do %>
<%= poetry_input_group_addon do %>
<%= poetry_input_group_text { "$" } %>
<% end %>
<%= poetry_input_group_input(name: "price", placeholder: "0.00", "aria-label": "Price") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_text { "USD" } %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_addon do %>
<%= poetry_input_group_text { "https://" } %>
<% end %>
<%= poetry_input_group_input(name: "domain", placeholder: "example.com", "aria-label": "Domain",
class: "pl-0.5") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_text { ".com" } %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_input(name: "username", placeholder: "Enter your username",
"aria-label": "Username") %>
<%= poetry_input_group_addon(align: :"inline-end") do %>
<%= poetry_input_group_text { "@company.com" } %>
<% end %>
<% end %>
<%= poetry_input_group do %>
<%= poetry_input_group_textarea(name: "message", placeholder: "Enter your message",
"aria-label": "Message") %>
<%= poetry_input_group_addon(align: :"block-end") do %>
<%= poetry_input_group_text(class: "text-xs text-muted-foreground") { "120 characters left" } %>
<% end %>
<% end %>
</div>
Textarea block
Open Textarea block standalone
Markdown supported
<%# The block form: a textarea with a full-width footer row. Block-aligned
addons (align: :"block-end") span the surface; poetry_input_group_text
gives the muted caption treatment. %>
<div class="w-80">
<%= poetry_label(for_id: "input-group-notes", class: "sr-only") { "Notes" } %>
<%= poetry_input_group do %>
<%= poetry_input_group_textarea(id: "input-group-notes", name: "notes", placeholder: "Add a note…") %>
<%= poetry_input_group_addon(align: :"block-end") do %>
<%= poetry_input_group_text { "Markdown supported" } %>
<% end %>
<% end %>
</div>
Textarea header
Open Textarea header standalone
Write a review
Up to 500 characters
<%# Block-aligned addons each take a full-width row. A header caption sits
above the control (align: :"block-start") and a footer pairs a hint with
a tiny action (align: :"block-end"). The textarea stays borderless so the
group owns the single surface. %>
<div class="w-80">
<%= poetry_label(for_id: "input-group-review", class: "sr-only") { "Review" } %>
<%= poetry_input_group do %>
<%= poetry_input_group_addon(align: :"block-start") do %>
<%= poetry_input_group_text { "Write a review" } %>
<% end %>
<%= poetry_input_group_textarea(id: "input-group-review", name: "review", placeholder: "Share what stood out…") %>
<%= poetry_input_group_addon(align: :"block-end") do %>
<%= poetry_input_group_text { "Up to 500 characters" } %>
<%= poetry_input_group_button(size: :xs, class: "ml-auto") { "Post" } %>
<% end %>
<% end %>
</div>
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=input-group] | The bordered group surface (role=group) - wears the border, the focus-within ring, and the invalid ring for the borderless control inside |
| [data-slot=input-group-addon] | One addon cell (icons, text, kbd hints, tiny buttons) rendered by poetry_input_group_addon around the control |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| input-group-addon | data-align | always - the addon's align: axis (inline rides the row, block takes a full-width row) | inline-start · inline-end · block-start · block-end |