Corex. Checkbox
(Corex v0.2.0)
View Source
Checkbox for Phoenix LiveView forms. Behavior follows Zag.js Checkbox.
Anatomy
Minimal
<.checkbox
id="terms" class="checkbox">
<:label>Option</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>Label and indicator
<.checkbox
id="terms" class="checkbox">
<:label>Accept the terms</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>Invalid
<.checkbox
id="terms"
class="checkbox ui-accent"
invalid
checked
errors={["Required"]}
>
<:label>Subscribe</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>Indeterminate
<.checkbox
id="terms" class="checkbox" checked={:indeterminate}>
<:label>Select some rows</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>API
Requires a stable id on <.checkbox id="terms">. Imperative helpers set or toggle checked state (boolean only; clears indeterminate).
| Function | Action | Returns |
|---|---|---|
set_checked/2 | Set checked state (client) | %Phoenix.LiveView.JS{} |
set_checked/3 | Set checked state (server) | socket |
toggle_checked/1 | Toggle checked state (client) | %Phoenix.LiveView.JS{} |
toggle_checked/2 | Toggle checked state (server) | socket |
set_checked
<.action phx-click={Corex.Checkbox.set_checked("checkbox-api-bind", true)} class="button ui-size-sm">
Set checked
</.action>
<.action phx-click={Corex.Checkbox.set_checked("checkbox-api-bind", false)} class="button ui-size-sm">
Set unchecked
</.action>
<.action phx-click={Corex.Checkbox.toggle_checked("checkbox-api-bind")} class="button ui-size-sm">
Toggle
</.action>
<.checkbox id="checkbox-api-bind" class="checkbox">
<:label>Terms</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>set_checked (dispatch)
const el = document.getElementById("checkbox-api-dispatch");
el?.dispatchEvent(
new CustomEvent("corex:checkbox:set-checked", { bubbles: false, detail: { checked: true } })
);
el?.dispatchEvent(
new CustomEvent("corex:checkbox:set-checked", { bubbles: false, detail: { checked: false } })
);
el?.dispatchEvent(new CustomEvent("corex:checkbox:toggle-checked", { bubbles: false }));def handle_event("check", %{"id" => id}, socket) do
{:noreply, Corex.Checkbox.set_checked(socket, id, true)}
end
def handle_event("uncheck", %{"id" => id}, socket) do
{:noreply, Corex.Checkbox.set_checked(socket, id, false)}
end
def handle_event("toggle", %{"id" => id}, socket) do
{:noreply, Corex.Checkbox.toggle_checked(socket, id)}
endEvents
User-driven only. Declarative checked may be true, false, or :indeterminate; imperative set_checked is always boolean.
Server events
| Event | When | Payload |
|---|---|---|
on_checked_change="checkbox_changed" | User toggles checked state | %{"id" => id, "checked" => boolean} |
on_checked_change
<.checkbox
id="terms"
class="checkbox"
on_checked_change="checkbox_changed"
>
<:label>Subscribe</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>def handle_event("checkbox_changed", %{"id" => id, "checked" => checked}, socket) do
{:noreply, assign(socket, :checked, checked)}
endClient events
| Event | When | event.detail |
|---|---|---|
on_checked_change_client="checkbox-changed" | User toggles checked state | id, checked |
on_checked_change_client
<.checkbox
id="checkbox-on-checked-change-client"
class="checkbox"
on_checked_change_client="checkbox-changed"
>
<:label>Subscribe</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>document.getElementById("checkbox-on-checked-change-client")?.addEventListener(
"checkbox-changed",
(event) => console.log(event.detail)
);Patterns
Async
Heex
<.async_result :let={checkbox} assign={@checkbox}>
<:loading><.checkbox_skeleton class="checkbox" /></:loading>
<.checkbox
id="terms" class="checkbox" checked={checkbox.checked}>
<:label>Accept terms</:label>
<:indicator><.heroicon name="hero-check" /></:indicator>
<:indeterminate><.heroicon name="hero-minus" /></:indeterminate>
</.checkbox>
</.async_result>Elixir
socket =
assign_async(socket, :checkbox, fn ->
Process.sleep(1000)
{:ok, %{checkbox: %{checked: true}}}
end)Controlled (LiveView)
Heex
<.checkbox
id="terms"
class="checkbox"
controlled
checked={@checked}
on_checked_change="patterns_controlled_changed"
>
<:label>Accept terms</:label>
<:indicator><.heroicon name="hero-check" /></:indicator>
<:indeterminate><.heroicon name="hero-minus" /></:indeterminate>
</.checkbox>Elixir
def mount(_params, _session, socket) do
{:ok, assign(socket, :checked, true)}
end
def handle_event("patterns_controlled_changed", %{"checked" => checked}, socket) do
{:noreply, assign(socket, :checked, checked)}
endStyle
Target parts with data-scope and data-part, or import checkbox.css and stack modifiers on the host.
[data-scope="checkbox"][data-part="root"] {}
[data-scope="checkbox"][data-part="control"] {}
[data-scope="checkbox"][data-part="label"] {}
[data-scope="checkbox"][data-part="hidden-input"] {}
[data-scope="checkbox"][data-part="error"] {}@import "../corex/corex.css";Stack modifiers on the host (class on <.checkbox id="terms">). Combine axes, for example checkbox ui-accent ui-size-lg.
Axes: Semantic (ui-accent, ui-brand, ui-alert, ui-info, ui-success), Size (ui-size-sm … ui-size-xl), Radius (ui-rounded-*). See the modifier guide.
Semantic modifiers set the checked control fill and indicator ink. Unchecked stays a neutral box; checked and indeterminate use the semantic fill with on-color ink. Checkbox has no variant axis.
Semantic
Palette for the checked control fill and indicator ink.
| Modifier | Classes |
|---|---|
| Default | checkbox |
| Accent | checkbox ui-accent |
| Brand | checkbox ui-brand |
| Alert | checkbox ui-alert |
| Info | checkbox ui-info |
| Success | checkbox ui-success |
<.checkbox
id="terms" class="checkbox" checked>
<:label>Default</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-accent" checked>
<:label>Accent</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-brand" checked>
<:label>Brand</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-alert" checked>
<:label>Alert</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-info" checked>
<:label>Info</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-success" checked>
<:label>Success</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>Size
| Modifier | Classes |
|---|---|
| SM | checkbox ui-size-sm |
| Default | checkbox |
| LG | checkbox ui-size-lg |
| XL | checkbox ui-size-xl |
<.checkbox
id="terms" class="checkbox ui-size-sm">
<:label>Small</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>
<.checkbox
id="terms" class="checkbox">
<:label>Default</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-size-lg">
<:label>Large</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>
<.checkbox
id="terms" class="checkbox ui-size-xl">
<:label>XLarge</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>Invalid
Invalid styles the label and control border. Checked indicators keep their semantic fill color.
<.checkbox
id="terms" class="checkbox ui-accent" invalid checked errors={["Required"]}>
<:label>Subscribe</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>Form
Set the form id in to_form/2 and use <.form for={@form}>. Use field={@form[:terms]} so the checkbox name matches the form. For Ecto validation in LiveView, add phx-change on the form so params stay in sync.
For cross-cutting invalid styling and error presentation, see the Forms guide. With field={@form[:…]}, pass auto_invalid for alert borders from visible errors, or invalid={true} to force the alert state.
Phoenix Form (changeset)
Heex
<.form
:let={f}
for={@form}
action="/account/terms"
method="post"
class="flex flex-col gap-space-lg w-full max-w-xl"
>
<.checkbox
id="terms" field={f[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.action type="submit" class="button ui-accent">
Submit
</.action>
</.form>Elixir
def account_terms_page(conn, _params) do
changeset = MyApp.Forms.Terms.changeset(%MyApp.Forms.Terms{}, %{})
form =
Phoenix.Component.to_form(changeset,
as: :terms_changeset,
id: "account-terms-changeset-form"
)
render(conn, :account_terms, form: form)
end
def account_terms_create(conn, %{"terms_changeset" => params}) do
case MyApp.Forms.Terms.changeset(%MyApp.Forms.Terms{}, params) do
%Ecto.Changeset{valid?: true} = changeset ->
data = Ecto.Changeset.apply_changes(changeset)
conn
|> put_flash(:info, "Saved: terms=#{data.terms}")
|> redirect(to: "/account")
changeset ->
changeset = Map.put(changeset, :action, :insert)
form =
Phoenix.Component.to_form(changeset,
as: :terms_changeset,
id: "account-terms-changeset-form"
)
render(conn, :account_terms, form: form)
end
endEcto
defmodule MyApp.Forms.Terms do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :terms, :boolean, default: false
end
def changeset(terms, attrs \ %{}) do
terms
|> cast(attrs, [:terms])
|> validate_required([:terms])
|> validate_acceptance(:terms)
end
def changeset_validate(terms, attrs \ %{}) do
terms
|> cast(attrs, [:terms])
|> validate_required([:terms], message: "can't be blank")
|> validate_acceptance(:terms, message: "must be accepted to continue")
end
endFor more form patterns (controller, LiveView, Ecto validation), see the Forms guide.
Summary
API
Set checked state from a control (phx-click). Clears indeterminate when applied.
Set checked state from handle_event. Pushes checkbox_set_checked (no reply event).
Set checked state for many checkboxes from handle_event. Pushes checkbox_set_checked_many (no reply event).
Toggle checked state from a control (phx-click).
Toggle checked from handle_event. Pushes checkbox_toggle_checked (no reply event).
Components
Renders a checkbox component.
Attributes
id(:string) - The id of the checkbox, useful for API to identify the checkbox. Defaults tonil.field(Phoenix.HTML.FormField) - A form field struct retrieved from the form, for example: @form[:email]. Automatically sets id, name, checked state, and errors from the form field. Defaults tonil.name(:string) - The name of the checkbox input for form submission. Defaults tonil.form(:string) - The form id to associate the checkbox with. Defaults tonil.invalid(:boolean) - Whether the control has validation errors. Defaults tonil.auto_invalid(:boolean) - When true withfield, set invalid from visible changeset errors (default false). Defaults tofalse.controlled(:boolean) - Whether the control is controlled. Defaults tofalse.disabled(:boolean) - Whether the control is disabled. Defaults tofalse.read_only(:boolean) - Whether the control is read-only. Defaults tofalse.required(:boolean) - Whether the control is required. Defaults tofalse.checked(:any) - Checked state: true, false, or :indeterminate (Zag CheckedState). Form fields still use boolean. Defaults tofalse.aria_label(:string) - The accessible label for the checkbox. Defaults to"Label".value(:string) - The value of the checkbox when checked. Defaults to"true".dir(:string) - The direction of the checkbox. When nil, derived from document (html lang + config :rtl_locales). Defaults tonil. Must be one ofnil,"ltr", or"rtl".orientation(:string) - Layout orientation for CSS (vertical or horizontal). Defaults to"horizontal". Must be one of"vertical", or"horizontal".on_checked_change(:string) - LiveView event when checked changes.handle_eventreceives%{"id" => id, "checked" => boolean}. Defaults tonil.on_checked_change_client(:string) - Browser event type on the checkbox element when checked changes.event.detail:{ id, checked }. Defaults tonil.errors(:list) - List of error messages to display. Defaults to[].- Global attributes are accepted.
Slots
label- Accepts attributes:class(:string)
indicator- Accepts attributes:class(:string)
indeterminate- Accepts attributes:class(:string)
error- Accepts attributes:class(:string)
Renders a loading skeleton for the checkbox component.
Attributes
skeleton_label(:boolean) - When true, renders a compact label-line placeholder (same line height band as the real checkbox label). Defaults totrue.dir(:string) - Same as checkbox: logical direction for layout. Defaults tonil. Must be one ofnil,"ltr", or"rtl".orientation(:string) - Same as checkbox: layout orientation for the skeleton root. Defaults to"horizontal". Must be one of"vertical", or"horizontal".- Global attributes are accepted.
API
Set checked state from a control (phx-click). Clears indeterminate when applied.
<.action phx-click={Corex.Checkbox.set_checked("my-checkbox", true)}>Check</.action>
<.checkbox id="my-checkbox" class="checkbox">
<:label>Option</:label>
</.checkbox>document.getElementById("my-checkbox")?.dispatchEvent(
new CustomEvent("corex:checkbox:set-checked", {
bubbles: false,
detail: { checked: true },
})
);
Set checked state from handle_event. Pushes checkbox_set_checked (no reply event).
<.action phx-click="check_box">Check</.action>
<.checkbox id="my-checkbox" class="checkbox">
<:label>Option</:label>
</.checkbox>def handle_event("check_box", _, socket) do
{:noreply, Corex.Checkbox.set_checked(socket, "my-checkbox", true)}
end
Set checked state for many checkboxes from handle_event. Pushes checkbox_set_checked_many (no reply event).
def handle_event("select_all", _, socket) do
ids = ["row-1", "row-2", "row-3"]
{:noreply, Corex.Checkbox.set_checked_many(socket, ids, true)}
end
Toggle checked state from a control (phx-click).
<.action phx-click={Corex.Checkbox.toggle_checked("my-checkbox")}>Toggle</.action>
<.checkbox id="my-checkbox" class="checkbox">
<:label>Option</:label>
</.checkbox>document.getElementById("my-checkbox")?.dispatchEvent(
new CustomEvent("corex:checkbox:toggle-checked", { bubbles: false })
);
Toggle checked from handle_event. Pushes checkbox_toggle_checked (no reply event).
<.action phx-click="toggle_box">Toggle</.action>
<.checkbox id="my-checkbox" class="checkbox">
<:label>Option</:label>
</.checkbox>def handle_event("toggle_box", _, socket) do
{:noreply, Corex.Checkbox.toggle_checked(socket, "my-checkbox")}
end