Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Forms

v1.1.0+
These components use the new `to_form/2` functionality
Full example checkbox group
Radio group
Radio card
Choose the plan that best suits your needs.
<.container max_width="md" class="mx-auto my-20">
<.h2>Simple form</.h2>
<.form for={@form} phx-submit="submit" phx-change="validate">
<.field
required
field={@form[:text]}
placeholder="Text"
phx-debounce="blur"
label="Text"
/>
<.field type="select" field={@form[:select]} options={["Option 1", "Option 2", "Option 3"]} />
<.field
type="checkbox-group"
field={@form[:checkbox_group]}
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>
<.field
type="radio-group"
field={@form[:radio_group]}
group_layout="row"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>
<.field
type="radio-card"
field={@form[:radio_card]}
label="Radio card"
options={[
%{label: "8-core CPU", value: "high", description: "32 GB RAM"},
%{label: "6-core CPU", value: "mid", description: "24 GB RAM"},
%{
label: "4-core CPU",
value: "low",
description: "16 GB RAM",
disabled: true
}
]}
size="sm"
variant="outline"
group_layout="row"
help_text="Choose the plan that best suits your needs."
required
/>
<.field type="switch" field={@form[:switch]} />
<.field type="checkbox" field={@form[:checkbox]} />
<.field type="color" field={@form[:color]} />
<.field type="date" field={@form[:date]} />
<.field type="datetime-local" field={@form[:datetime_local]} />
<.field type="email" field={@form[:email]} />
<.field type="file" field={@form[:file]} />
<.field type="hidden" field={@form[:hidden]} />
<.field type="month" field={@form[:month]} />
<.field type="number" field={@form[:number]} />
<.field type="password" field={@form[:password]} />
<.field type="range" field={@form[:range]} />
<.field type="search" field={@form[:search]} />
<.field type="tel" field={@form[:tel]} />
<.field type="time" field={@form[:time]} />
<.field type="url" field={@form[:url]} />
<.field type="week" field={@form[:week]} />
<.field
field={form[:text]}
placeholder="Enter text"
label="Clearable"
type="text"
clearable
/>
<.field
field={form[:text]}
placeholder="This is just a placeholder"
value="https://example.com/invite/your-invite-code"
label="Copyable"
copyable
/>
<.field
type="password"
field={form[:password]}
label="Viewable"
placeholder="Placeholder"
viewable
/>
<div class="flex justify-end">
<.button>Submit</.button>
</div>
</.form>
</.container>
defmodule PetalProWeb.SimpleFormLive do
use PetalProWeb, :live_view
@impl true
def mount(_params, _session, socket) do
changeset = build_changeset()
{:ok, assign_form(socket, changeset)}
end
@impl true
def handle_event("validate", %{"object" => object_params}, socket) do
changeset =
object_params
|> build_changeset()
|> Map.put(:action, :validate)
{:noreply, assign_form(socket, changeset)}
end
@impl true
def handle_event("submit", %{"object" => object_params}, socket) do
changeset = build_changeset(object_params)
case validate_changeset(changeset) do
{:ok, _object} ->
socket =
socket
|> put_flash(:success, "Object successfully created")
|> assign_form(build_changeset())
{:noreply, socket}
{:error, changeset} ->
socket =
socket
|> put_flash(:error, inspect(changeset.errors))
{:noreply, assign_form(socket, changeset)}
end
end
defp assign_form(socket, changeset) do
assign(socket, form: to_form(changeset, as: :object))
end
defp build_changeset(params \ %{}) do
data = %{}
types = %{
text: :string,
select: :string,
checkbox_group: {:array, :string},
radio_group: :string,
radio_card: :string,
textarea: :string,
checkbox: :boolean,
color: :string,
date: :date,
datetime_local: :naive_datetime,
email: :string,
file: :string,
hidden: :string,
month: :string,
number: :integer,
password: :string,
radio: :string,
range: :integer,
search: :string,
tel: :string,
time: :time,
url: :string,
week: :string,
switch: :boolean
}
{data, types}
|> Ecto.Changeset.cast(params, Map.keys(types))
|> Ecto.Changeset.validate_required([:text])
|> Ecto.Changeset.validate_acceptance(:checkbox)
|> Ecto.Changeset.validate_length(:text, min: 3, max: 50)
end
defp validate_changeset(changeset) do
Ecto.Changeset.apply_action(changeset, :validate)
end
end

Field

`.field` is the easiest way to create input fields. We support all HTML input types plus a couple more.
Favourite pudding

not a pudding!

Gender
<.form for={@form}>
<.field field={@form[:name]} />
<.field type="email" field={@form[:email]} />
<!-- Without a field -->
<.field label="Favourite pudding" value="mcdonalds" name="favourite_pudding" errors={["not a pudding!"]} />
<.field type="password" field={@form[:password]} />
<.field
type="radio-group"
options={[Male: "male", Female: "female", Other: "other"]}
field={@form[:gender]}
/>
<div class="flex justify-end gap-3">
<.button type="button" label="Cancel" color="white" />
<.button type="submit" label="Submit" />
</div>
</.form>

Field properties

attr :id, :any,
default: nil,
doc: "the id of the input. If not passed, it will be generated automatically from the field"
attr :name, :any,
doc: "the name of the input. If not passed, it will be generated automatically from the field"
attr :label, :string,
doc:
"the label for the input. If not passed, it will be generated automatically from the field"
attr :value, :any,
doc:
"the value of the input. If not passed, it will be generated automatically from the field"
attr :type, :string,
default: "text",
values:
~w(checkbox checkbox-group color date datetime-local email file hidden month number password
range radio-group radio-card search select switch tel text textarea time url week),
doc: "the type of input"
attr :size, :string,
default: "md",
values: ~w(xs sm md lg xl),
doc: "the size of the switch (xs, sm, md, lg or xl) or radio card (sm, md or lg)"
attr :variant, :any, default: "outline", doc: "outline, classic - used by radio-card"
attr :viewable, :boolean,
default: false,
doc: "If true, adds a toggle to show/hide the password text"
attr :copyable, :boolean,
default: false,
doc: "If true, adds a copy button to the field and disables the input"
attr :clearable, :boolean,
default: false,
doc: "If true, adds a clear button to clear the field value"
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list,
default: [],
doc:
"a list of errors to display. If not passed, it will be generated automatically from the field. Format is a list of strings."
attr :checked, :any, doc: "the checked flag for checkboxes and checkbox groups"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :disabled_options, :list, default: [], doc: "the options to disable in a checkbox group"
attr :group_layout, :string,
values: ["row", "col"],
default: "row",
doc: "the layout of the inputs in a group (checkbox-group or radio-group)"
attr :empty_message, :string,
default: nil,
doc:
"the message to display when there are no options available, for checkbox-group or radio-group"
attr :rows, :string, default: "4", doc: "rows for textarea"
attr :class, :any, default: nil, doc: "the class to add to the input"
attr :wrapper_class, :any, default: nil, doc: "the wrapper div classes"
attr :help_text, :string, default: nil, doc: "context/help for your field"
attr :label_class, :any, default: nil, doc: "extra CSS for your label"
attr :selected, :any, default: nil, doc: "the selected value for select inputs"
attr :required, :boolean,
default: false,
doc: "is this field required? is passed to the input and adds an asterisk next to the label"
attr :rest, :global,
include:
~w(autocomplete autocorrect autocapitalize disabled form max maxlength min minlength list
pattern placeholder readonly required size step value name multiple prompt default year month day hour minute second builder options layout cols rows wrap checked accept),
doc: "All other props go on the input"

Clearable

<.field field={@form[:text]} placeholder="Enter text" label="Clearable" type="text" clearable />

See all field properties

Copyable

<.field
field={@form[:text]}
placeholder="This is just a placeholder"
value="https://example.com/invite/your-invite-code"
label="Copyable"
copyable
/>

See all field properties

Viewable

<.field
type="password"
field={@form[:password]}
label="Viewable"
placeholder="Placeholder"
viewable
/>

See all field properties

Checkbox

<.field field={@form[:field_name]} type="checkbox" />
<-- With help text -->
<.field field={@form[:field_name]} type="checkbox" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="checkbox" />

See all field properties

Checkbox group

Checkbox group
Checkbox group col layout
Checkbox group help text
Here is some help text
Checkbox group disabled options
The second option is disabled
<.field
field={@form[:field_name]}
type="checkbox-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>
<!-- With col layout -->
<.field
field={@form[:field_name]}
type="checkbox-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
group_layout="col"
/>
<!-- With help text -->
<.field
field={@form[:field_name]}
type="checkbox-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
help_text="Here is some help text"
/>
<!-- With disabled options -->
<.field
field={@form[:field_name]}
type="checkbox-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
help_text="The second option is disabled"
disabled_options={["2"]}
/>

See all field properties

Color input

Here is some help text
<.field field={@form[:field_name]} type="color" />
<!-- With help text -->
<.field field={@form[:field_name]} type="color" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="color" />

See all field properties

Date input

Here is some help text
<.field field={@form[:field_name]} type="date" />
<!-- With help text -->
<.field field={@form[:field_name]} type="date" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="date" />

See all field properties

Datetime local input

Here is some help text
<.field field={@form[:field_name]} type="datetime-local" />
<!-- With help text -->
<.field field={@form[:field_name]} type="datetime-local" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="datetime-local" />

See all field properties

Email input

Here is some help text
<.field field={@form[:field_name]} type="email" />
<!-- With help text -->
<.field field={@form[:field_name]} type="email" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="email" />

See all field properties

File input

Here is some help text
<.field field={@form[:field_name]} type="file" />
<!-- With help text -->
<.field field={@form[:field_name]} type="file" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="file" />

See all field properties

Hidden input

<!-- This just renders the hidden input with no label -->
<.field field={@form[:field_name]} type="hidden" />

See all field properties

Number input

Here is some help text
<.field field={@form[:field_name]} type="number" />
<!-- With help text -->
<.field field={@form[:field_name]} type="number" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="number" />

See all field properties

Password input

Here is some help text
<.field field={@form[:field_name]} type="password" />
<!-- With help text -->
<.field field={@form[:field_name]} type="password" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="password" />

See all field properties

Radio group

Radio group
Field name col layout
Radio group help text
Here is some help text
<.field
field={@form[:field_name]}
type="radio-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>
<!-- With a row layout -->
<.field
field={@form[:field_name]}
type="radio-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
group_layout="col"
/>
<!-- With help text -->
<.field
field={@form[:field_name]}
type="radio-group"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
help_text="Here is some help text"
/>

Radio card

New
Radio card
Choose the plan that best suits your needs.
Radio card
Choose the plan that best suits your needs.
Radio card
Choose the plan that best suits your needs.
<.field
field={@form[:radio_card]}
type="radio-card"
label="Radio card"
options={[
%{label: "8-core CPU", value: "high", description: "32 GB RAM"},
%{label: "6-core CPU", value: "mid", description: "24 GB RAM"},
%{
label: "4-core CPU",
value: "low",
description: "16 GB RAM",
disabled: true
}
]}
size="sm"
variant="outline"
group_layout="row"
help_text="Choose the plan that best suits your needs."
required
/>
<.field
field={@form[:radio_card]}
type="radio-card"
label="Radio card"
options={[
%{label: "8-core CPU", value: "high", description: "32 GB RAM"},
%{label: "6-core CPU", value: "mid", description: "24 GB RAM"},
%{
label: "4-core CPU",
value: "low",
description: "16 GB RAM",
disabled: true
}
]}
size="md"
variant="classic"
group_layout="row"
help_text="Choose the plan that best suits your needs."
required
/>
<.field
field={@form[:radio_card]}
type="radio-card"
label="Radio card"
options={[
%{label: "8-core CPU", value: "high", description: "32 GB RAM"},
%{label: "6-core CPU", value: "mid", description: "24 GB RAM"},
%{
label: "4-core CPU",
value: "low",
description: "16 GB RAM",
disabled: true
}
]}
size="lg"
variant="outline"
group_layout="row"
help_text="Choose the plan that best suits your needs."
required
/>

See all field properties

Range input

Here is some help text
<.field field={@form[:field_name]} type="range" />
<!-- With help text -->
<.field field={@form[:field_name]} type="range" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="range" />

See all field properties

Dual Range Slider

A two-handle range slider for selecting a min/max window. Requires PetalDualRangeSlider from the JS bundle. Requires min_field and max_field instead of a single field.

Price range
$0 $0 – $1000 $1000
Match score
0% 0% – 100% 100%
<!-- Basic usage with value_prefix -->
<.field
type="range-dual"
label="Price range"
min_field={@form[:price_min]}
max_field={@form[:price_max]}
range_min={0}
range_max={1000}
value_prefix="$"
/>
<!-- With a suffix and custom labels -->
<.field
type="range-dual"
label="Match score"
min_field={@form[:score_min]}
max_field={@form[:score_max]}
range_min={0}
range_max={100}
value_suffix="%"
range_min_label="0%"
range_max_label="100%"
/>
<!-- Plain input without label/errors wrapper -->
<.input
type="range-dual"
min_field={@form[:price_min]}
max_field={@form[:price_max]}
range_min={0}
range_max={500}
/>

Register the hook in your LiveSocket:

import PetalComponents from "../../deps/petal_components/assets/js/petal_components"
const liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...PetalComponents }
})

See all field properties

Here is some help text
<.field field={@form[:field_name]} type="search" />
<!-- With help text -->
<.field field={@form[:field_name]} type="search" help_text="Here is some help text" />
<-- Just in the input -->
<.input field={@form[:field_name]} type="search" />

See all field properties

Select

Here is some help text
<.field
field={@form[:field_name]}
type="select"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>
<!-- With a prompt -->
<.field
field={@form[:field_name]}
type="select"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
prompt="Select an option"
/>
<!-- With help text -->
<.field
field={@form[:field_name]}
type="select"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
help_text="Here is some help text"
/>
<!-- With multiple -->
<.field
field={@form[:field_name]}
type="select"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
multiple={true}
/>
<!-- Just in the input -->
<.input
field={@form[:field_name]}
type="select"
options={[{"Option 1", "1"}, {"Option 2", "2"}, {"Option 3", "3"}]}
/>

See all field properties

Switch

Here is some help text
<.field
type="switch"
field={@form[:xs]}
size="xs"
/>
<.field
type="switch"
field={@form[:sm]}
size="sm"
/>
<.field
type="switch"
field={@form[:md]}
size="md"
/>
<.field
type="switch"
field={@form[:lg]}
size="lg"
/>
<.field
type="switch"
field={@form[:xl]}
size="xl"
/>
<!-- With help text -->
<.field field={@form[:field_name]} type="switch" help_text="Here is some help text" />
<!-- Just in the input -->
<.input field={@form[:field_name]} type="switch" />

See all field properties

Telephone input

Here is some help text
<.field field={@form[:field_name]} type="tel" />
<!-- With help text -->
<.field field={@form[:field_name]} type="tel" help_text="Here is some help text" />
<!-- Just in the input -->
<.input field={@form[:field_name]} type="tel" />

See all field properties

Text input

Here is some help text
<.field field={@form[:field_name]} />
<!-- With a placeholder -->
<.field field={@form[:field_name]} placeholder="eg. John" />
<!-- With help text -->
<.field field={@form[:field_name]} help_text="Here is some help text" />
<!-- Just the input -->
<.input field={@form[:field_name]} placeholder="eg. John" />

See all field properties

Textarea

Here is some help text
<.field field={@form[:field_name]} type="textarea" />
<!-- With help text -->
<.field field={@form[:field_name]} type="textarea" help_text="Here is some help text" />
<!-- Just the input -->
<.input field={@form[:field_name]} type="textarea" />

See all field properties

Time input

Here is some help text
<.field field={@form[:field_name]} type="time" />
<!-- With help text -->
<.field field={@form[:field_name]} type="time" help_text="Here is some help text" />
<!-- Just in the input -->
<.input field={@form[:field_name]} type="time" />

See all field properties

URL input

Here is some help text
<.field field={@form[:field_name]} type="url" />
<!-- With help text -->
<.field field={@form[:field_name]} type="url" help_text="Here is some help text" />
<!-- Just in the input -->
<.input field={@form[:field_name]} type="url" />

Field label

My label
<.field_label>My label</.field_label>
attr :for, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true

Field error

My Error

<.field_error>My Error</.field_error>
slot :inner_block, required: true

See all field properties