Form-related components modeled after shadcn/ui.
Included components:
Summary
Functions
Renders a filterable autocomplete input backed by a hidden form value.
Renders a checkbox control with optional inline label content.
Field wrapper for label, control, description, and errors.
Wraps the main interactive control inside a field.
Helper text shown beneath a field control.
Error or validation message shown beneath a field control.
Neutral status or informational message shown beneath a field control.
Renders an input with shadcn classes.
Wraps an input and sibling controls (buttons/icons) in a single inline group.
Text/icon/status/action segment used inside input_group/1.
Compact action button for use inside input_group_addon/1.
Styled text segment for use inside input_group_addon/1.
Renders an OTP-style segmented input layout.
Renders a form label.
Renders a native <select> element with shadcn styles.
Renders a number input with increment and decrement controls.
Renders a radio group with native radio inputs.
Renders a custom select with a button trigger and listbox content.
Renders a slider using native range input(s).
Renders a switch control with optional label content.
Renders a textarea with shadcn classes.
Functions
Renders a filterable autocomplete input backed by a hidden form value.
This is intended for searching and selecting from a known set of options. Use
select/1 when you want a trigger-driven listbox instead of a text input.
## When to use it
Use autocomplete/1 when the person typing should search by label, but the
form needs to submit a separate stable value through the hidden input.
Prefer combobox/1 for simpler label-in/label-out filtering where the typed
text itself is the selected value and you do not need a separate hidden form
field.
For server-backed search, keep the current query in LiveView assigns and
update the option list on phx-change or phx-input. The component keeps
its hidden value form-friendly while the visible input remains a normal text
field that can participate in debounced LiveView events.
## Examples
heex title="Autocomplete" align="full" <.autocomplete id="team-owner" name="owner" value="levi"> <:option value="levi" label="Levi Buzolic" description="Engineering" /> <:option value="mira" label="Mira Chen" description="Design" /> <:option value="sam" label="Sam Hall" description="Operations" /> </.autocomplete>
heex title="Loading state" align="full" vrt <.autocomplete id="repo-search" name="repo" loading={true}> <:option value="cinder" label="cinder_ui" /> </.autocomplete>
heex title="LiveView server search" align="full" vrt <.form for={%{}} phx-change="search-owners"> <.autocomplete id="owner-search" name="owner" value="mira" placeholder="Search teammates..." loading={false} phx-debounce="300" aria-label="Search owners" > <:option value="levi" label="Levi Buzolic" /> <:option value="mira" label="Mira Chen" /> <:empty>No teammates match the current query.</:empty> </.autocomplete> </.form>
### With FormField
<.autocomplete field={@form[:owner]}>
<:option value="levi" label="Levi Buzolic" />
<:option value="mira" label="Mira Chen" />
</.autocomplete>### With label
<.autocomplete field={@form[:owner]} label="Owner">
<:option value="levi" label="Levi Buzolic" />
</.autocomplete>### With explicit errors
<.autocomplete field={@form[:owner]} label="Owner" errors={["is required"]}>
<:option value="levi" label="Levi Buzolic" />
</.autocomplete>### Inside field composition
<.field>
<:label for={@form[:owner].id}>Owner</:label>
<.autocomplete field={@form[:owner]}>
<:option value="levi" label="Levi Buzolic" />
</.autocomplete>
<:description>Assign a teammate to this project.</:description>
</.field>Screenshot

Renders a checkbox control with optional inline label content.
## Examples
heex title="Basic checkbox" align="full" <.checkbox id="terms" name="terms">Accept terms</.checkbox>
heex title="Checked state" align="full" <.checkbox id="updates" name="updates" checked={true}>Notify me about product updates</.checkbox>
### With FormField
<.checkbox field={@form[:active]} />### With label attr (inline)
<.checkbox field={@form[:active]} label="Active" />### With inner_block (takes precedence over label attr)
<.checkbox field={@form[:terms]}>
I agree to the <a href="/terms">Terms of Service</a>
</.checkbox>### With explicit errors
<.checkbox field={@form[:terms]} errors={["must be accepted"]} />### Inside field composition
<.field>
<:label for={@form[:active].id}>Active</:label>
<.checkbox field={@form[:active]} />
</.field>Screenshot

Field wrapper for label, control, description, and errors.
field/1 remains the simplest composition helper. It automatically wraps the
control passed to its inner block with field_control/1, so most usages
should pass the form control directly and use the :label, :description,
:message, and :error slots for supporting content.
Prefer the shorthand :label slot with for for ordinary field labels.
Reach for raw :label slot content, field_label/1, field_control/1,
field_description/1, field_message/1, and field_error/1 when you need
richer markup or want to compose the pieces outside field/1.
## Examples
heex title="Profile field" align="full" <.field> <:label for="name">Name</:label> <.input id="name" name="name" /> <:description>Shown in your profile.</:description> </.field>
heex title="Validation state" align="full" vrt <.field> <:label for="email">Work email</:label> <.input id="email" name="email" type="email" /> <:description>We'll send deployment alerts here.</:description> <:error>Please use your company domain.</:error> </.field>
heex title="Custom Label Markup" align="full" <.field invalid={true}> <:label> <.field_label> <.label for="workspace-slug">Workspace slug</.label> <span class="text-muted-foreground text-xs">Used in your public workspace URL.</span> </.field_label> </:label> <.input id="workspace-slug" name="workspace[slug]" value="cinder-ui" /> <:error>Slug has already been taken.</:error> </.field>
heex title="Phoenix validation flow" align="full" vrt <.form for={%{}} phx-change="validate" phx-submit="save" class="space-y-6"> <.field invalid={true}> <:label for="owner">Owner</:label> <.autocomplete id="owner" name="owner" value="levi" aria-label="Owner" > <:option value="levi" label="Levi Buzolic" description="Engineering" /> <:option value="mira" label="Mira Chen" description="Design" /> <:empty>No matching teammates.</:empty> </.autocomplete> <:description>Choose the teammate responsible for this workspace.</:description> <:error>Please choose a teammate.</:error> </.field> </.form>
heex title="Date range fields" align="full" <div class="grid gap-4 sm:grid-cols-2"> <.field> <:label for="report_start_date">Start date</:label> <.input id="report_start_date" name="report[start_date]" type="date" value="2026-06-01" /> <:description>Use the first local day to include in the report.</:description> </.field> <.field invalid={true}> <:label for="report_end_date">End date</:label> <.input id="report_end_date" name="report[end_date]" type="date" value="2026-05-31" min="2026-06-01" aria-invalid="true" /> <:error>End date must be on or after the start date.</:error> </.field> </div>
heex title="LiveView date validation" align="full" vrt <.form for={@form} phx-change="validate" phx-submit="save" class="grid gap-6"> <.field> <:label for={@form[:start_date].id}>Start date</:label> <.input field={@form[:start_date]} type="date" required /> <:description>Changes are validated by the LiveView on phx-change.</:description> </.field> <.field invalid={true}> <:label for={@form[:end_date].id}>End date</:label> <.input field={@form[:end_date]} type="date" min="2026-06-01" aria-invalid="true" /> <:error>End date must be on or after the start date.</:error> </.field> </.form>
Screenshot

Wraps the main interactive control inside a field.
field/1 already applies this wrapper around its inner block, so you
generally do not need to call field_control/1 inside a normal field/1
example. Use it directly when composing a field manually or when you need to
attach the invalid-state control styles outside field/1.
## Example
heex title="Field control wrapper" align="full" <.field_control> <.input id="workspace_slug" value="cinder-ui" /> </.field_control>
heex title="Field control with helper text" align="full" <.field> <:label for="billing_email">Billing email</:label> <.input id="billing_email" name="billing[email]" type="email" placeholder="billing@team.com" /> <:description>Invoices and payment reminders go here.</:description> </.field>
Screenshot

Helper text shown beneath a field control.
In most field/1 usage, prefer the :description slot. Use
field_description/1 directly for isolated helper rendering or custom field
composition.
## Example
heex title="Field description" align="full" <.field_description>Used in your public workspace URL.</.field_description>
heex title="Field description in context" align="full" <.field> <:label for="workspace_slug">Workspace slug</:label> <.input id="workspace_slug" name="workspace[slug]" value="cinder-ui" /> <:description>Used in your public workspace URL.</:description> </.field>
Screenshot

Error or validation message shown beneath a field control.
In most field/1 usage, prefer the :error slot. Use field_error/1
directly for isolated helper rendering or custom field composition.
## Example
heex title="Field error" align="full" <.field_error>Please use your company domain.</.field_error>
heex title="Field error in context" align="full" <.field invalid={true}> <:label for="work_email">Work email</:label> <.input id="work_email" name="work_email" type="email" value="hello@gmail.com" /> <:error>Please use your company domain.</:error> </.field>
Screenshot

Wraps field labels so shared spacing and invalid-state styling remain
consistent across controls. Inside field/1, provide it via the :label
slot when you need richer label content than a single label/1.
## Example
heex title="Grouped field label" align="full" <.field_label> <.label for="workspace_name">Workspace name</.label> <span class="text-muted-foreground text-xs">Used across the dashboard.</span> </.field_label>
heex title="Field label in context" align="full" <.field> <:label> <.field_label> <.label for="workspace_name">Workspace name</.label> <span class="text-muted-foreground text-xs">Shown in team switchers.</span> </.field_label> </:label> <.input id="workspace_name" name="workspace[name]" value="Cinder UI" /> </.field>
Screenshot

Neutral status or informational message shown beneath a field control.
In most field/1 usage, prefer the :message slot. Use field_message/1
directly for isolated helper rendering or custom field composition.
## Example
heex title="Field message" align="full" <.field_message>Visible immediately after save.</.field_message>
heex title="Field message in context" align="full" <.field> <:label for="project_name">Project name</:label> <.input id="project_name" name="project[name]" value="Marketing site refresh" /> <:message>Saved automatically a few seconds ago.</:message> </.field>
Screenshot

Renders an input with shadcn classes.
## Examples
heex title="Text input" align="full" <.input id="email" type="email" placeholder="name@example.com" />
heex title="With value" align="full" <.input id="username" name="username" value="levi" />
### With FormField
<.input field={@form[:email]} />### With label
<.input field={@form[:email]} label="Email" />### With explicit errors
<.input field={@form[:email]} label="Email" errors={["can't be blank"]} />### Inside field composition
<.field>
<:label for={@form[:email].id}>Email</:label>
<.input field={@form[:email]} />
<:description>We'll send alerts here.</:description>
</.field>Screenshot

Wraps an input and sibling controls (buttons/icons) in a single inline group.
Use input_group_addon/1 for static text, icons, status copy, or compact
buttons that should visually attach to the grouped controls.
## Examples
heex title="Search with action" align="full" <.input_group> <.input placeholder="Search" /> <.input_group_addon> <.input_group_button variant={:secondary}>Go</.input_group_button> </.input_group_addon> </.input_group>
heex title="Handle input" align="full" <.input_group> <.input placeholder="organization" /> <.input_group_addon> <.input_group_text>@company.com</.input_group_text> </.input_group_addon> </.input_group>
heex title="URL builder" align="full" <.input_group> <.input_group_addon> <.input_group_text>https://</.input_group_text> </.input_group_addon> <.input value="cinder-ui" /> <.input_group_addon> <.input_group_text>.com</.input_group_text> </.input_group_addon> </.input_group>
heex title="Command search" align="full" <.input_group> <.input_group_addon> <.icon name="search" class="size-4" /> </.input_group_addon> <.input placeholder="Search components" /> <.input_group_addon> <.kbd>⌘K</.kbd> </.input_group_addon> </.input_group>
heex title="Loading state" align="full" <.input_group> <.input placeholder="Generating invite link..." disabled /> <.input_group_addon> <.spinner class="size-4" /> <.input_group_text>Syncing</.input_group_text> </.input_group_addon> </.input_group>
heex title="Select + input" align="full" <.input_group> <.native_select name="team-role" value="admin" class="w-32" aria-label="Team role"> <:option value="admin" label="Admin" /> <:option value="editor" label="Editor" /> <:option value="viewer" label="Viewer" /> </.native_select> <.input placeholder="email@example.com" type="email" class="flex-1" /> </.input_group>
heex title="Textarea with footer action" align="full" <.input_group align={:block_end}> <.textarea rows={3} placeholder="Write a comment..." class="min-h-[5.5rem]" /> <.input_group_addon align={:block_end}> <span>0/280</span> <.button size={:sm}>Post</.button> </.input_group_addon> </.input_group>
heex title="Copy URL action" align="full" <.input_group> <.input placeholder="https://example.com" /> <.input_group_addon> <.input_group_button variant={:outline}>Copy</.input_group_button> </.input_group_addon> </.input_group>
heex title="Icon actions" align="full" <.input_group> <.input value="ck_live_************************" readonly /> <.input_group_addon> <.input_group_button size={:icon_xs} aria-label="Reveal key"> <.icon name="eye" /> </.input_group_button> <.input_group_button size={:icon_xs} aria-label="Copy key"> <.icon name="copy" /> </.input_group_button> </.input_group_addon> </.input_group>
Screenshot

Text/icon/status/action segment used inside input_group/1.
This is useful for prefixes, suffixes, inline status, and small utility content that should attach to the surrounding grouped field.
## Example
heex title="Input group addon" align="full" <.input_group> <.input_group_addon> <.icon name="mail" class="size-4" /> </.input_group_addon> <.input type="email" placeholder="team@example.com" /> </.input_group>
Screenshot

Compact action button for use inside input_group_addon/1.
This is intentionally smaller than the general button/1 API. Use it for
short text actions and icon-only utility actions embedded in an input group.
## Example
heex title="Input group button" align="full" <.input_group> <.input placeholder="Search" /> <.input_group_addon> <.input_group_button>Search</.input_group_button> </.input_group_addon> </.input_group>
Screenshot

Styled text segment for use inside input_group_addon/1.
Use this for prefixes, suffixes, units, and short status text when the addon also contains icons or buttons.
## Example
heex title="Input group text" align="full" <.input_group> <.input placeholder="Amount" /> <.input_group_addon> <.input_group_text>USD</.input_group_text> </.input_group_addon> </.input_group>
Screenshot

Renders an OTP-style segmented input layout.
This component renders one input per position and can be wired using standard
Phoenix input names such as code[]. The bundled CuiInputOtp hook adds
auto-advance, backspace-to-previous, and paste distribution behavior.
## Examples
heex title="Basic OTP input" align="full" <.input_otp name="verification_code[]" length={6} />
heex title="With grouped separators" align="full" <.input_otp name="backup_code[]" length={6} groups={[3, 3]} values={["1", "2", "3", "4", "5", "6"]} />
### With FormField (string value is split into individual cells)
<.input_otp field={@form[:code]} length={6} />### With label
<.input_otp field={@form[:code]} label="Verification code" length={6} />### With explicit errors
<.input_otp field={@form[:code]} label="Verification code" errors={["is invalid"]} length={6} />### Inside field composition
<.field>
<:label for={@form[:code].id}>Verification code</:label>
<.input_otp field={@form[:code]} length={6} />
<:description>Enter the 6-digit code from your email.</:description>
</.field>Screenshot

Renders a form label.
## Examples
<.label for="email">Email</.label>
<.label for="project_name">
Project name
<span class="text-destructive">*</span>
</.label>Screenshot

Renders a native <select> element with shadcn styles.
Use this when you want platform-native select behavior rather than the custom
listbox UI from select/1.
## Examples
heex title="Native select" align="full" <.native_select name="framework" value="phoenix"> <:option value="phoenix" label="Phoenix" /> <:option value="rails" label="Rails" /> <:option value="laravel" label="Laravel" /> </.native_select>
heex title="With placeholder" align="full" <.native_select name="assignee" placeholder="Assign a teammate"> <:option value="levi" label="Levi" /> <:option value="juz" label="Justin" /> </.native_select>
### With FormField (using options attr)
<.native_select field={@form[:role]} options={[{"Admin", "admin"}, {"Member", "member"}]} />### With FormField (using option slots)
<.native_select field={@form[:role]}>
<:option value="admin" label="Admin" />
<:option value="member" label="Member" />
</.native_select>### With label
<.native_select field={@form[:role]} label="Role" options={[{"Admin", "admin"}]} />### With explicit errors
<.native_select field={@form[:role]} label="Role" errors={["is required"]} options={[{"Admin", "admin"}]} />### Inside field composition
<.field>
<:label for={@form[:role].id}>Role</:label>
<.native_select field={@form[:role]} options={[{"Admin", "admin"}]} />
<:description>Choose your access level.</:description>
</.field>Screenshot

Renders a number input with increment and decrement controls.
Keyboard interaction comes from the native type="number" input, so arrow
keys, min/max constraints, and step behavior stay browser-native while the
buttons provide a touch-friendly affordance.
## Examples
heex title="Basic number field" align="full" <.number_field id="seat-count" name="seats" value={3} min={1} max={10} />
heex title="Fractional step" align="full" <.number_field id="discount" name="discount" value={1.5} min={0} max={5} step={0.5} />
### With FormField
<.number_field field={@form[:quantity]} />### With label
<.number_field field={@form[:quantity]} label="Quantity" />### With explicit errors
<.number_field field={@form[:quantity]} label="Quantity" errors={["must be positive"]} />### Inside field composition
<.field>
<:label for={@form[:quantity].id}>Quantity</:label>
<.number_field field={@form[:quantity]} />
<:description>Enter a positive number.</:description>
</.field>Screenshot

Renders a radio group with native radio inputs.
## Examples
heex title="Basic radio group" align="full" <.radio_group name="plan" value="pro"> <:option value="free" label="Free" /> <:option value="pro" label="Pro" /> </.radio_group>
heex title="With disabled option" align="full" <.radio_group name="region" value="us"> <:option value="us" label="United States" /> <:option value="eu" label="Europe" disabled={true} /> </.radio_group>
### With FormField
<.radio_group field={@form[:plan]}>
<:option value="free" label="Free" />
<:option value="pro" label="Pro" />
</.radio_group>### With label (renders as fieldset/legend, not label/for)
<.radio_group field={@form[:plan]} label="Choose a plan">
<:option value="free" label="Free" />
<:option value="pro" label="Pro" />
</.radio_group>### With explicit errors
<.radio_group field={@form[:plan]} label="Choose a plan" errors={["is required"]}>
<:option value="free" label="Free" />
<:option value="pro" label="Pro" />
</.radio_group>### Inside field composition
<.field>
<:label for={@form[:plan].id}>Plan</:label>
<.radio_group field={@form[:plan]}>
<:option value="free" label="Free" />
<:option value="pro" label="Pro" />
</.radio_group>
</.field>Screenshot

Renders a custom select with a button trigger and listbox content.
Use native_select/1 when you specifically want a plain HTML <select>.
## Examples
heex title="Custom select" align="full" <.select id="team-plan" name="plan" value="pro"> <:option value="free" label="Free" /> <:option value="pro" label="Pro" /> <:option value="enterprise" label="Enterprise" /> </.select>
heex title="Grouped labels" align="full" vrt <.select id="assignee" name="assignee" placeholder="Assign a teammate"> <:option value="levi" label="Levi" description="Platform" group="Engineering" /> <:option value="mira" label="Mira" description="Product Design" group="Design" /> </.select>
heex title="Disabled option" align="full" vrt <.select id="region" name="region"> <:option value="us" label="United States" /> <:option value="eu" label="Europe" /> <:option value="apac" label="APAC" disabled={true} /> </.select>
heex title="Clearable select" align="full" vrt <.select id="support-tier" name="tier" value="pro" clearable={true}> <:option value="free" label="Free" /> <:option value="pro" label="Pro" /> </.select>
### With FormField
<.select field={@form[:role]}>
<:option value="admin" label="Admin" />
<:option value="member" label="Member" />
</.select>### With label
<.select field={@form[:role]} label="Role">
<:option value="admin" label="Admin" />
<:option value="member" label="Member" />
</.select>### With explicit errors
<.select field={@form[:role]} label="Role" errors={["is required"]}>
<:option value="admin" label="Admin" />
</.select>### Inside field composition
<.field>
<:label for={@form[:role].id}>Role</:label>
<.select field={@form[:role]}>
<:option value="admin" label="Admin" />
</.select>
<:description>Choose your access level.</:description>
</.field>Screenshot

Renders a slider using native range input(s).
Use min, max, and step for scalar values. For range sliders, render two
controls and sync values in LiveView.
## Examples
heex title="Basic slider" align="full" <.slider id="volume" name="volume" value={45} min={0} max={100} step={1} />
heex title="CPU limit slider" align="full" <.slider id="cpu_limit" name="cpu_limit" value={2} min={1} max={8} step={1} />
### With FormField
<.slider field={@form[:volume]} />### With label
<.slider field={@form[:volume]} label="Volume" />### With explicit errors
<.slider field={@form[:volume]} label="Volume" errors={["is required"]} />### Inside field composition
<.field>
<:label for={@form[:volume].id}>Volume</:label>
<.slider field={@form[:volume]} />
<:description>Drag to adjust volume level.</:description>
</.field>Screenshot

Renders a switch control with optional label content.
## Examples
heex title="Basic switch" align="full" <.switch id="marketing" checked={true}>Email updates</.switch>
heex title="Disabled" align="full" <.switch id="notifications" disabled={true}>Push notifications</.switch>
### With FormField
<.switch field={@form[:notifications]} />### With label attr (inline)
<.switch field={@form[:notifications]} label="Enable notifications" />### With inner_block (takes precedence over label attr)
<.switch field={@form[:notifications]}>
Enable <strong>push</strong> notifications
</.switch>### With explicit errors
<.switch field={@form[:notifications]} errors={["is required"]} />### Inside field composition
<.field>
<:label for={@form[:notifications].id}>Notifications</:label>
<.switch field={@form[:notifications]} />
</.field>Screenshot

Renders a textarea with shadcn classes.
## Examples
heex title="Basic textarea" align="full" <.textarea id="bio" name="bio" rows={4} />
heex title="With placeholder" align="full" <.textarea id="release_notes" name="release_notes" rows={8} placeholder="Summarize what changed in this release..." />
### With FormField
<.textarea field={@form[:bio]} />### With label
<.textarea field={@form[:bio]} label="Bio" />### With explicit errors
<.textarea field={@form[:bio]} label="Bio" errors={["too short"]} />### Inside field composition
<.field>
<:label for={@form[:bio].id}>Bio</:label>
<.textarea field={@form[:bio]} />
<:description>Tell us about yourself.</:description>
</.field>Screenshot
