Input
A form control for entering a single line of text.
Installation
Included in poetry-ui — available as
poetry_input
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
Default
<%# Standalone inputs carry an aria-label; inside a form, use the
FormBuilder's field instead (it wires ids, errors, and aria). %>
<div class="w-80">
<%= poetry_input(type: "email", name: "email", placeholder: "you@example.com",
"aria-label": "Email") %>
</div>
Button group
<%# Upstream input-button-group: the input and its action FUSED into one
control - ButtonGroup collapses the borders between neighbors - the
attached counterpart to the Inline example's separate row. The Field
wires the label to the input inside the group. %>
<div class="w-80">
<%= poetry_field(id: "input-button-group", label_text: "Search") do |field| %>
<%= poetry_button_group do %>
<%= poetry_input(name: "query", placeholder: "Type to search...",
**field.control_attributes.transform_keys(&:to_sym)) %>
<%= poetry_button(variant: :outline) { "Search" } %>
<% end %>
<% end %>
</div>
Disabled
<%= poetry_input(name: "plan", value: "Enterprise", disabled: true,
"aria-label": "Plan", class: "w-80") %>
Field
Choose a unique username for your account.
<%# Upstream input-field: the Field quartet around an input - label,
control, and hint wired by control_attributes. The full family
(fieldsets, groups, choice cards) lives on the Field page. %>
<div class="w-80">
<%= poetry_field(id: "input-field-username", label_text: "Username",
hint: "Choose a unique username for your account.") do |field| %>
<%= poetry_input(name: "username", placeholder: "Enter your username",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
File
<%# type: "file" renders the native picker - it still gets a Label
wired via for_id, since placeholder text is never the label. %>
<div class="grid w-80 gap-2">
<%= poetry_label(for_id: "input-resume") { "Upload resume" } %>
<%= poetry_input(type: "file", name: "resume", id: "input-resume") %>
</div>
Form
<%# Upstream input-form: a whole form through FieldGroup - stacked
fields, a two-column row pairing phone and country, a plain actions
row (field horizontal is the boolean-control layout, not a generic
row). Name is required: aria-required rides the field, no native
attribute, no asterisk - upstream's own treatment here. %>
<form class="w-full max-w-sm">
<%= poetry_field_group do %>
<%= poetry_field(id: "form-name", label_text: "Name", required: true) do |field| %>
<%= poetry_input(name: "name", placeholder: "Evil Rabbit",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
<%= poetry_field(id: "form-email", label_text: "Email",
hint: "We'll never share your email with anyone.") do |field| %>
<%= poetry_input(type: "email", name: "email", placeholder: "john@example.com",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
<div class="grid grid-cols-2 gap-4">
<%= poetry_field(id: "form-phone", label_text: "Phone") do |field| %>
<%= poetry_input(type: "tel", name: "phone", placeholder: "+1 (555) 123-4567",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
<%= poetry_field(id: "form-country", label_text: "Country") do |field| %>
<%= poetry_select(name: "country", value: "us",
**field.control_attributes.transform_keys(&:to_sym)) do |select| %>
<% select.with_item(value: "us") { "United States" } %>
<% select.with_item(value: "uk") { "United Kingdom" } %>
<% select.with_item(value: "ca") { "Canada" } %>
<% end %>
<% end %>
</div>
<%= poetry_field(id: "form-address", label_text: "Address") do |field| %>
<%= poetry_input(name: "address", placeholder: "123 Main St",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
<div class="flex items-center gap-2">
<%= poetry_button(variant: :outline, type: :button) { "Cancel" } %>
<%= poetry_button(type: :submit) { "Submit" } %>
</div>
<% end %>
</form>
Grid
<%# Two inputs share one row via a two-column grid - each still
carries its own Label wired by for_id. %>
<div class="grid w-96 grid-cols-2 gap-4">
<div class="grid gap-2">
<%= poetry_label(for_id: "input-first-name") { "First name" } %>
<%= poetry_input(name: "first_name", id: "input-first-name",
placeholder: "Ada") %>
</div>
<div class="grid gap-2">
<%= poetry_label(for_id: "input-last-name") { "Last name" } %>
<%= poetry_input(name: "last_name", id: "input-last-name",
placeholder: "Lovelace") %>
</div>
</div>
Inline
<%# Upstream input-inline: the input and its action on one row. poetry's
field orientation: :horizontal is the boolean-control layout (control
left, label stack right), so an inline pair is a plain flex row - the
input flexes, the button holds the trailing edge. %>
<div class="flex w-96 items-center gap-2">
<%= poetry_input(type: "search", name: "q", placeholder: "Search...",
"aria-label": "Search", class: "flex-1") %>
<%= poetry_button { "Search" } %>
</div>
Invalid
<%# Error styling comes from aria-invalid - in real forms the
Field/FormBuilder sets it from model errors, never by hand. %>
<div class="w-80">
<%= poetry_input(type: "email", name: "email", value: "not-an-email",
invalid: true, "aria-label": "Email") %>
</div>
Required
This field must be filled out.
<%# Upstream input-required through the Field quartet. The asterisk is
the sighted mark only (aria-hidden); field required: carries the
announcement as aria-required on the control - never the native
attribute (no UA bubbles, no double announcement). %>
<div class="w-80">
<%= poetry_field(id: "input-required", required: true,
hint: "This field must be filled out.") do |field| %>
<%= poetry_label(for_id: "input-required") do %>
Required Field <span class="text-destructive" aria-hidden="true">*</span>
<% end %>
<%= poetry_input(name: "required_field", placeholder: "This field is required",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
With badge
<%# Upstream input-badge: the label row carries a Badge at the trailing
edge - the label stretches across the field's grid, so ml-auto lands
the pill at the far side. The badge text joins the accessible name
("Webhook URL Beta"), same as upstream. %>
<div class="w-80">
<%= poetry_field(id: "input-badge") do |field| %>
<%= poetry_label(for_id: "input-badge") do %>
Webhook URL
<%= poetry_badge(variant: :secondary, class: "ml-auto") { "Beta" } %>
<% end %>
<%= poetry_input(type: "url", name: "webhook_url",
placeholder: "https://api.example.com/webhook",
**field.control_attributes.transform_keys(&:to_sym)) %>
<% end %>
</div>
With value
<%= poetry_input(name: "city", value: "Lisbon", "aria-label": "City", class: "w-80") %>
API
Poetry::Ui::Input::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 |
|---|---|---|---|
| disabled: | Boolean | defaults to false |
Disables the native input. |
| invalid: | Boolean | defaults to false |
Marks the input aria-invalid - the error skin keys on the attribute. |
| mask: | String | Format-as-you-type mask descriptor ('(999) 999-9999'). Extra knobs (slot char, always-show, auto-clear) ride Stimulus values via data: - one declarative option covers the common case. | |
| name: | String | The submitted param name. | |
| placeholder: | String | Native placeholder text - not a substitute for a Label. | |
| type: | String | defaults to "text" |
The native type attribute (text, email, password, file, ...). |
| value: | String | The current value. |
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] | The <input> element itself - no inner anatomy; error state is aria-invalid (set by Field/FormBuilder), never a parallel class |
State attributes
| Part | Attribute | Condition | Values |
|---|---|---|---|
| input | data-raw | mask: is set - the unmasked value, kept live by the mask controller (the masked text is what submits) | — |