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.Forms

Table of Contents

Core

Form Layout

Form Feedback

Advanced Selects

Stepped Forms

Specialised Form Inputs


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>