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.
Add search-only terms with the option's :keywords attribute. Keywords are
not rendered or announced and are only considered when the visible label
does not match the current query.
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" keywords={["backend", "Elixir"]} /> <: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>
heex title="Autocomplete popup" align="full" vrt <div class="pb-48"> <.autocomplete id="country-search" name="country" value="au" variant={:popup} placeholder="Search countries..." open={true} > <:option value="au" label="Australia" /> <:option value="nz" label="New Zealand" /> <:option value="us" label="United States" /> </.autocomplete> </div>
heex title="Grouped autocomplete" align="full" vrt <div class="pb-48"> <.autocomplete id="timezone-search" name="timezone" placeholder="Search timezones..." open={true} > <:option value="nyc" label="(GMT-5) New York" group="Americas" /> <:option value="lax" label="(GMT-8) Los Angeles" group="Americas" /> <:option value="lon" label="(GMT+0) London" group="Europe" /> </.autocomplete> </div>
heex title="Custom autocomplete items" align="full" vrt <div class="pb-36"> <.autocomplete id="framework-search" name="framework" open={true} > <:option value="next" label="Next.js"> <span class="flex flex-col"> <span class="font-medium">Next.js</span> <span class="text-muted-foreground text-xs">React framework</span> </span> </:option> </.autocomplete> </div>
### 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.
Plain inputs, selects, native selects, and both autocomplete variants become seamless automatically when they are direct children. The group owns their border, radius, shadow, and focus ring while popup content remains visible.
## 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" vrt <.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="Autocomplete input group" align="full" vrt <div class="pb-40"> <.input_group> <.input_group_addon> <.input_group_text>Owner</.input_group_text> </.input_group_addon> <.autocomplete id="group-owner-search" name="owner" placeholder="Search teammates..." open={true} > <:option value="levi" label="Levi Buzolic" /> <:option value="mira" label="Mira Chen" /> <:option value="sam" label="Sam Hall" /> </.autocomplete> </.input_group> </div>
heex title="Autocomplete popup input group" align="full" vrt <div class="pb-40"> <.input_group> <.input_group_addon> <.input_group_text>Country</.input_group_text> </.input_group_addon> <.autocomplete id="group-country-search" name="country" value="au" variant={:popup} placeholder="Search countries..." open={true} > <:option value="au" label="Australia" /> <:option value="nz" label="New Zealand" /> <:option value="us" label="United States" /> </.autocomplete> </.input_group> </div>
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
