Forms Cheatsheet
View SourceQuick reference for form controls and patterns in Sutra UI.
Basic Form
LiveView Form with Changeset
<.form for={@form} class="form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} type="email" label="Email" />
<.button type="submit" phx-disable-with="Saving...">Save</.button>
</.form>Manual Form
<.form for={@form} phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.button type="submit">Save</.button>
</.form>Text Inputs
Basic Input
<.input type="text" name="name" value={@name} />
<.input type="text" name="name" placeholder="Enter name" />
<.input type="text" name="name" disabled />With Label and Description
<.input
field={@form[:name]}
label="Full Name"
description="Enter your legal name"
/>Input with Errors
<.input
field={@form[:email]}
type="email"
label="Email"
errors={@errors}
/>Input Types Reference
| Type | Example |
|---|---|
text | <.input type="text" /> |
email | <.input type="email" /> |
password | <.input type="password" /> |
number | <.input type="number" min="0" /> |
tel | <.input type="tel" /> |
url | <.input type="url" /> |
search | <.input type="search" /> |
date | <.input type="date" /> |
time | <.input type="time" /> |
datetime-local | <.input type="datetime-local" /> |
Textarea
Basic Textarea
<.textarea name="bio" />
<.textarea name="bio" rows="6" />
<.textarea name="bio" placeholder="Tell us about yourself..." />With Character Count
<.input
field={@form[:bio]}
type="textarea"
label="Bio"
description={"#{String.length(@bio || "")}/500 characters"}
maxlength="500"
/>Input Groups
With Prefix
<.input_group>
<:prefix>https://</:prefix>
<.input type="text" name="domain" placeholder="example.com" />
</.input_group>With Suffix
<.input_group>
<.input type="number" name="weight" />
<:suffix>kg</:suffix>
</.input_group>With Icons
<.input_group>
<:prefix>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</:prefix>
<.input type="search" name="q" placeholder="Search..." />
</.input_group>
<.input_group>
<:prefix>$</:prefix>
<.input type="number" name="price" />
<:suffix>.00</:suffix>
</.input_group>Selection Controls
Checkbox
<.checkbox id="terms" name="terms" />
<!-- With label -->
<div class="flex items-center gap-2">
<.checkbox id="terms" name="terms" />
<.label for="terms">I agree to the terms</.label>
</div>
<!-- Checked by default -->
<.checkbox id="subscribe" name="subscribe" checked />Checkbox Group Pattern
<fieldset>
<legend class="text-sm font-medium">Notifications</legend>
<div class="space-y-2 mt-2">
<div class="flex items-center gap-2">
<.checkbox id="email" name="notifications[]" value="email" />
<.label for="email">Email</.label>
</div>
<div class="flex items-center gap-2">
<.checkbox id="sms" name="notifications[]" value="sms" />
<.label for="sms">SMS</.label>
</div>
</div>
</fieldset>Switch
<.switch id="dark-mode" name="dark_mode" />
<!-- With label -->
<div class="flex items-center gap-2">
<.switch id="notifications" name="notifications" />
<.label for="notifications">Enable notifications</.label>
</div>Radio Group
<.radio_group name="plan" value={@selected_plan}>
<:radio value="free">Free</:radio>
<:radio value="pro">Pro - $10/mo</:radio>
<:radio value="enterprise">Enterprise - Contact us</:radio>
</.radio_group>Radio with Descriptions
<.radio_group name="shipping">
<:radio value="standard">
<span class="font-medium">Standard</span>
<span class="text-muted-foreground">3-5 business days</span>
</:radio>
<:radio value="express">
<span class="font-medium">Express</span>
<span class="text-muted-foreground">1-2 business days</span>
</:radio>
</.radio_group>Select Controls
Basic Select
<.select
id="country"
name="country"
placeholder="Select a country"
options={[
{"United States", "us"},
{"Canada", "ca"},
{"Mexico", "mx"}
]}
/>Select with Groups
<.select
id="timezone"
name="timezone"
placeholder="Select timezone"
options={[
{"Americas": [
{"New York", "America/New_York"},
{"Los Angeles", "America/Los_Angeles"}
]},
{"Europe": [
{"London", "Europe/London"},
{"Paris", "Europe/Paris"}
]}
]}
/>Select with Default Value
<.select
id="status"
name="status"
value={@status}
options={[
{"Active", "active"},
{"Inactive", "inactive"},
{"Pending", "pending"}
]}
/>Live Select (Async Search)
<.form for={@form} class="form" phx-change="validate">
<.live_component
module={SutraUI.LiveSelect}
id="user-select"
field={@form[:user_id]}
placeholder="Search users..."
/>
</.form>
<%= if user = SutraUI.LiveSelect.decode(@form.params["user_id"]) do %>
Selected user id: {user}
<% end %>In your LiveView:
def handle_event("live_select_change", %{"text" => text, "id" => id}, socket) do
users = Accounts.search_users(text)
options = Enum.map(users, &{&1.name, &1.id})
send_update(SutraUI.LiveSelect, id: id, options: options)
{:noreply, socket}
endSliders
Basic Slider
<.slider id="volume" name="volume" min="0" max="100" value="50" />Slider with Steps
<.slider id="rating" name="rating" min="1" max="5" step="1" value="3" />Range Slider (Dual Handle)
<.range_slider
id="price-range"
name="price"
min="0"
max="1000"
min_value="200"
max_value="800"
/>Slider with Labels
<div class="space-y-2">
<.input
field={@form[:volume]}
type="number"
label={"Volume: #{@volume}%"}
min="0"
max="100"
phx-change="update_volume"
/>
<.slider id="volume" name="volume" min="0" max="100" value={@volume} />
</div>Form Patterns
Inline Validation
# LiveView
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end<.form for={@form} class="form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} type="email" label="Email" />
<.button type="submit">Save</.button>
</.form>Form with Loading State
<.form for={@form} class="form" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.button type="submit" loading={@saving} phx-disable-with="Saving...">Save</.button>
</.form>Multi-Step Form
<.tabs id="form-steps" default_value={@step}>
<:tab value="1" disabled={@step < 1}>Step 1</:tab>
<:tab value="2" disabled={@step < 2}>Step 2</:tab>
<:tab value="3" disabled={@step < 3}>Review</:tab>
<:panel value="1">
<.form for={@form} class="form" phx-submit="next_step">
<.input field={@form[:name]} label="Name" />
<.button type="submit">Next</.button>
</.form>
</:panel>
<!-- Additional panels... -->
</.tabs>Filter Forms
Filter Bar
<.filter_bar>
<.input type="search" name="q" value={@query} placeholder="Search..." />
<.select
id="status"
name="status"
value={@status}
options={[{"All", ""}, {"Active", "active"}, {"Inactive", "inactive"}]}
/>
<.select
id="sort"
name="sort"
value={@sort}
options={[{"Newest", "newest"}, {"Oldest", "oldest"}, {"Name", "name"}]}
/>
<.button type="submit">Apply</.button>
<.button type="button" variant="outline" phx-click="reset_filters">
Reset
</.button>
</.filter_bar>Date Range Filter
<.filter_bar>
<.input type="date" name="start_date" value={@start_date} />
<span class="text-muted-foreground">to</span>
<.input type="date" name="end_date" value={@end_date} />
<.button type="submit">Filter</.button>
</.filter_bar>Error Handling
Input with Error
<.input
field={@form[:email]}
type="email"
label="Email"
phx-debounce="blur"
errors={Enum.map(@form[:email].errors, &translate_error/1)}
/>Form-Level Errors
<.form for={@form} class="form" phx-submit="save">
<.alert :if={@form.errors[:base]} variant="destructive">
{translate_error(@form.errors[:base])}
</.alert>
<.input field={@form[:email]} label="Email" />
<.button type="submit">Save</.button>
</.form>Async Validation
def handle_event("validate_email", %{"email" => email}, socket) do
case Accounts.email_available?(email) do
true -> {:noreply, assign(socket, email_error: nil)}
false -> {:noreply, assign(socket, email_error: "Email already taken")}
end
end<.input
field={@form[:email]}
type="email"
label="Email"
phx-blur="validate_email"
phx-debounce="500"
errors={if @email_error, do: [@email_error], else: []}
/>