34 form components — layout, fieldsets, checkbox groups, radio cards, cascaders, and transfer lists. These are the structural building blocks for complex forms. Pair them with inputs.md for actual input elements.
Modules: PhiaUi.Components.Forms, PhiaUi.Components.FormLayout, PhiaUi.Components.FormSelects
import PhiaUi.Components.FormsTable of Contents
Core
Form Layout
Form Feedback
Advanced Selects
- checkbox_group
- form_checkbox_group
- radio_card
- radio_card_group
- form_radio_card_group
- cascader
- form_cascader
- button_transfer_list
Stepped Forms
Specialised Form Inputs
- currency_input / form_currency_input
- masked_input / form_masked_input
- range_slider / form_range_slider
- signature_pad
- color_swatch_picker / form_color_swatch_picker
- float_input / form_float_input
- float_textarea / form_float_textarea
- country_select / form_country_select
form
Phoenix <.form> wrapper with optional error summary and consistent spacing.
<.form for={@form} phx-change="validate" phx-submit="save" class="space-y-6">
<.phia_input field={@form[:name]} label="Full name" required />
<.phia_input field={@form[:email]} type="email" label="Email" />
<.form_actions>
<.button variant="outline" phx-click="cancel" type="button">Cancel</.button>
<.button type="submit">Save changes</.button>
</.form_actions>
</.form>Form field integration
Every form input works in two modes:
Standalone — pass name and value directly:
<.input type="text" name="query" value={@query} placeholder="Search…" />Ecto-integrated — pass field and get label, validation, error display automatically:
<.phia_input field={@form[:email]} type="email" label="Email" required />The field attr accepts Phoenix.HTML.FormField — PhiaUI reads .name, .value, .errors, and .id automatically.
field
Bare field wrapper with label, description, and error slot — use to build custom inputs.
<.field label="Custom widget" description="Configure your preferences.">
<:input>
<MyApp.CustomWidget name={@form[:widget].name} value={@form[:widget].value} />
</:input>
<:error :for={e <- @form[:widget].errors}><%= translate_error(e) %></:error>
</.field>form_section
Groups a set of fields with a title and optional description. Use to divide long forms into logical blocks.
<.form for={@form} phx-submit="save">
<.form_section title="Personal information" description="This is how others will see you.">
<.phia_input field={@form[:name]} label="Full name" />
<.phia_input field={@form[:bio]} type="textarea" label="Bio" />
</.form_section>
<.form_section title="Account settings">
<.phia_input field={@form[:email]} type="email" label="Email" />
<.phia_input field={@form[:username]} label="Username" />
</.form_section>
<.form_actions>
<.button type="submit">Save profile</.button>
</.form_actions>
</.form>form_fieldset
Named group for related fields — renders a <fieldset> with <legend>.
<.form_fieldset legend="Shipping address">
<.phia_input field={@form[:address]} label="Street" />
<.form_grid cols={2}>
<.phia_input field={@form[:city]} label="City" />
<.phia_input field={@form[:postcode]} label="Postcode" />
</.form_grid>
</.form_fieldset>form_grid
Responsive column grid for form fields.
<.form_grid cols={2}>
<.phia_input field={@form[:first_name]} label="First name" />
<.phia_input field={@form[:last_name]} label="Last name" />
</form_grid>
<.form_grid cols={3}>
<.phia_input field={@form[:city]} label="City" />
<.phia_input field={@form[:state]} label="State" />
<.phia_input field={@form[:zip]} label="ZIP" />
</.form_grid>Attrs: cols (1–4, default 1), gap (integer, default 4)
form_row
Side-by-side label + input layout (common in settings forms).
<.form_row label="Email notifications" description="Receive email updates about your account activity.">
<.switch name="email_notif" checked={@user.email_notif} phx-change="toggle_email" />
</.form_row>form_actions
Sticky bottom action bar for forms.
<.form_actions sticky={true}>
<.button variant="outline" phx-click="cancel" type="button">Discard</.button>
<.button type="submit">Save changes</.button>
</.form_actions>Attrs: sticky (boolean, uses sticky bottom-0)
form_summary
Inline error summary listing all validation errors.
<.form_summary form={@form} />Renders a <ul> of all field errors — useful for long forms where inline errors may scroll out of view.
checkbox_group
Group of checkboxes with optional select-all.
<.checkbox_group
label="Permissions"
name="permissions"
values={@selected_permissions}
options={[{"Read", "read"}, {"Write", "write"}, {"Delete", "delete"}]}
phx-change="update_permissions"
/>form_checkbox_group
Ecto-integrated checkbox group.
<.form_checkbox_group
field={@form[:roles]}
label="Roles"
options={Enum.map(@all_roles, &{&1.name, &1.id})}
/>radio_card / radio_card_group
Card-style radio buttons — great for plan, theme, or layout pickers.
<.form for={@form} phx-submit="save">
<.radio_card_group label="Select your plan">
<.radio_card
name="plan"
value="starter"
checked={@form[:plan].value == "starter"}
title="Starter"
description="For individuals and small teams."
price="$9/mo"
/>
<.radio_card
name="plan"
value="pro"
checked={@form[:plan].value == "pro"}
title="Pro"
description="For growing teams."
price="$29/mo"
/>
</.radio_card_group>
</.form>cascader
Hierarchical multi-level select (province → city → district). Hook: PhiaCascader.
<.cascader
id="location"
name="location"
options={@location_tree}
placeholder="Select location…"
phx-change="set_location"
/>options is a nested list: %{label, value, children: [...]}.
button_transfer_list
Move items between "available" and "selected" lists.
<.button_transfer_list
id="feature-flags"
name="features"
available={@available_features}
selected={@selected_features}
on_change="update_features"
/>form_stepper
Multi-step form with navigation and validation per step.
<.form_stepper current_step={@step} on_next="next_step" on_prev="prev_step">
<.form_stepper_item step={1} label="Account" valid={@step > 1}>
<.phia_input field={@form[:email]} type="email" label="Email" />
<.phia_input field={@form[:password]} type="password" label="Password" />
</.form_stepper_item>
<.form_stepper_item step={2} label="Profile">
<.phia_input field={@form[:name]} label="Full name" />
<.phia_input field={@form[:bio]} type="textarea" label="Bio" />
</.form_stepper_item>
<.form_stepper_item step={3} label="Confirm">
<.form_summary form={@form} />
</.form_stepper_item>
</.form_stepper>currency_input / form_currency_input
Number input formatted as currency with symbol.
<.currency_input name="price" value={@price} currency="USD" phx-change="set_price" />
<.form_currency_input field={@form[:price]} label="Price" currency="EUR" />masked_input / form_masked_input
Input with format mask (phone, date, card number). Hook: PhiaMaskedInput.
<.masked_input name="phone" value={@phone} mask="+1 (999) 999-9999" />
<.form_masked_input field={@form[:date_of_birth]} label="Date of birth" mask="99/99/9999" />range_slider / form_range_slider
Dual-handle range slider. Hook: PhiaRangeSlider.
<.range_slider
id="price-range"
name_from="price_min"
name_to="price_max"
from={@price_min}
to={@price_max}
min={0}
max={1000}
step={10}
phx-change="set_range"
/>signature_pad
Canvas signature capture. Hook: PhiaSignaturePad.
<.signature_pad id="sig-pad" name="signature" phx-change="update_sig" />color_swatch_picker / form_color_swatch_picker
Preset colour swatches with optional custom hex input.
<.color_swatch_picker
name="accent"
value={@accent_color}
swatches={["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6", "#8b5cf6"]}
phx-change="set_accent"
/>float_input / form_float_input
Material-style floating label input — label animates up on focus.
<.float_input id="email-float" name="email" type="email" label="Email address" value={@email} />
<.form_float_input field={@form[:name]} label="Full name" />float_textarea / form_float_textarea
Material-style floating label textarea.
<.form_float_textarea field={@form[:message]} label="Your message" rows={5} />country_select / form_country_select
Select with 250 countries and flag emoji.
<.country_select name="country" value={@country} phx-change="set_country" />
<.form_country_select field={@form[:country]} label="Country" />Real-world: Settings page form
<.form for={@form} phx-change="validate" phx-submit="save" id="profile-form">
<.form_section title="Public profile" description="This information will be displayed publicly.">
<.form_grid cols={2}>
<.phia_input field={@form[:first_name]} label="First name" required />
<.phia_input field={@form[:last_name]} label="Last name" required />
</.form_grid>
<.phia_input field={@form[:username]} label="Username" />
<.phia_input field={@form[:bio]} type="textarea" label="Bio" rows={3} />
<.phia_input field={@form[:website]} type="url" label="Website" />
</.form_section>
<.form_section title="Notifications">
<.form_row label="Email updates" description="Receive news and product updates via email.">
<.switch name="notif_email" checked={@form[:notif_email].value} phx-change="toggle" />
</.form_row>
<.form_row label="Weekly digest" description="A summary of activity every Monday.">
<.switch name="notif_digest" checked={@form[:notif_digest].value} phx-change="toggle" />
</.form_row>
</.form_section>
<.form_actions sticky={true}>
<.button variant="outline" phx-click="reset" type="button">Discard changes</.button>
<.button type="submit" disabled={not @form.source.valid?}>Save profile</.button>
</.form_actions>
</.form>