Corex. Checkbox
(Corex v0.1.2)
View Source
Phoenix implementation of Zag.js Checkbox.
Anatomy
Minimal
<.checkbox class="checkbox">
<:label>Option</:label>
</.checkbox>Label and indicator
<.checkbox class="checkbox">
<:label>Accept the terms</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
</.checkbox>Invalid
<.checkbox
class="checkbox checkbox--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 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>. 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 button--sm">
Set checked
</.action>
<.action phx-click={Corex.Checkbox.set_checked("checkbox-api-bind", false)} class="button button--sm">
Set unchecked
</.action>
<.action phx-click={Corex.Checkbox.toggle_checked("checkbox-api-bind")} class="button button--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
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 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
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/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/checkbox.css";Color
| Modifier | Classes |
|---|---|
| Default | checkbox |
| Accent | checkbox checkbox--accent |
| Brand | checkbox checkbox--brand |
| Alert | checkbox checkbox--alert |
| Info | checkbox checkbox--info |
| Success | checkbox checkbox--success |
<.checkbox class="checkbox" checked>
<:label>Default</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox class="checkbox checkbox--accent" checked>
<:label>Accent</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox class="checkbox checkbox--brand" checked>
<:label>Brand</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox class="checkbox checkbox--alert" checked>
<:label>Alert</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox class="checkbox checkbox--info" checked>
<:label>Info</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>
<.checkbox class="checkbox checkbox--success" checked>
<:label>Success</:label>
<:indicator>
<.heroicon name="hero-check" />
</:indicator>
<:indeterminate>
<.heroicon name="hero-minus" />
</:indeterminate>
</.checkbox>Size
| Modifier | Classes |
|---|---|
| SM | checkbox checkbox--sm |
| Default | checkbox |
| LG | checkbox checkbox--lg |
| XL | checkbox checkbox--xl |
<.checkbox class="checkbox checkbox--sm">
<:label>Small</:label>
</.checkbox>
<.checkbox class="checkbox">
<:label>Default</:label>
</.checkbox>
<.checkbox class="checkbox checkbox--lg">
<:label>Large</:label>
</.checkbox>
<.checkbox class="checkbox checkbox--xl">
<:label>XLarge</:label>
</.checkbox>Invalid
Invalid styles the label and control border. Checked indicators keep their semantic fill color.
<.checkbox class="checkbox checkbox--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. Pass invalid={Corex.FormField.invalid?(@form[:terms])} when you want alert borders after validation.
Phoenix Form (changeset)
Heex
<.form
:let={f}
for={@form}
action="/account/terms"
method="post"
>
<.checkbox field={f[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.action type="submit" class="button button--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
endEcto changeset (validation)
Heex
<.form
:let={f}
for={@form}
action="/account/terms"
method="post"
>
<.checkbox field={f[:terms]} class="checkbox">
<:label>Accept terms (strict messages)</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.action type="submit" class="button button--accent">
Submit
</.action>
</.form>Elixir
def account_terms_strict_page(conn, _params) do
changeset =
MyApp.Forms.Terms.changeset_validate(%MyApp.Forms.Terms{}, %{})
form =
Phoenix.Component.to_form(changeset,
as: :terms_validate,
id: "account-terms-validate-form"
)
render(conn, :account_terms_strict, form: form)
end
def account_terms_strict_create(conn, %{"terms_validate" => params}) do
case MyApp.Forms.Terms.changeset_validate(%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_validate,
id: "account-terms-validate-form"
)
render(conn, :account_terms_strict, 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
endNative form (plain HTML)
<form
action="/register"
method="post"
>
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<.checkbox
name="user[accept_terms]"
class="checkbox"
>
<:label>Accept terms</:label>
</.checkbox>
<.action type="submit" class="button button--accent">Submit</.action>
</form>LiveView · Phoenix Form (changeset)
Heex
<.form
for={@form}
phx-change="validate"
phx-submit="save"
>
<.checkbox field={@form[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.action type="submit" class="button button--accent">
Submit
</.action>
</.form>Elixir
def mount(_params, _session, socket) do
form =
%MyApp.Forms.Terms{}
|> MyApp.Forms.Terms.changeset(%{})
|> Phoenix.Component.to_form(as: :terms)
{:ok, assign(socket, :form, form)}
end
def handle_event("validate", %{"terms" => params}, socket) do
changeset =
%MyApp.Forms.Terms{}
|> MyApp.Forms.Terms.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, Phoenix.Component.to_form(changeset, action: :validate, as: :terms))}
end
def handle_event("save", %{"terms" => params}, socket) do
case MyApp.Forms.Terms.changeset(%MyApp.Forms.Terms{}, params) do
%Ecto.Changeset{valid?: true} = _changeset ->
{:noreply, assign(socket, :form, Phoenix.Component.to_form(MyApp.Forms.Terms.changeset(%MyApp.Forms.Terms{}, %{}), as: :terms))}
changeset ->
{:noreply, assign(socket, :form, Phoenix.Component.to_form(changeset, action: :insert, as: :terms))}
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
endLiveView · Ecto Changeset (validation)
Heex
<.form
for={@form}
phx-change="validate_strict"
phx-submit="save_strict"
>
<.checkbox field={@form[:terms]} class="checkbox">
<:label>Accept terms</:label>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.checkbox>
<.action type="submit" class="button button--accent">
Submit
</.action>
</.form>Elixir
def mount(_params, _session, socket) do
form =
%MyApp.Forms.Terms{}
|> MyApp.Forms.Terms.changeset_validate(%{})
|> Phoenix.Component.to_form(as: :terms_strict)
{:ok, assign(socket, :strict_form, form)}
end
def handle_event("validate_strict", %{"terms_strict" => params}, socket) do
changeset =
%MyApp.Forms.Terms{}
|> MyApp.Forms.Terms.changeset_validate(params)
|> Map.put(:action, :validate)
{:noreply,
assign(socket, :strict_form, Phoenix.Component.to_form(changeset, action: :validate, as: :terms_strict))}
end
def handle_event("save_strict", %{"terms_strict" => params}, socket) do
case MyApp.Forms.Terms.changeset_validate(%MyApp.Forms.Terms{}, params) do
%Ecto.Changeset{valid?: true} = _changeset ->
{:noreply,
assign(
socket,
:strict_form,
Phoenix.Component.to_form(MyApp.Forms.Terms.changeset_validate(%MyApp.Forms.Terms{}, %{}), as: :terms_strict)
)}
changeset ->
{:noreply, assign(socket, :strict_form, Phoenix.Component.to_form(changeset, action: :insert, as: :terms_strict))}
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
end
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).
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.checked(:any) - Checked state: true, false, or :indeterminate (Zag CheckedState). Form fields still use boolean. Defaults tofalse.controlled(:boolean) - Whether the checkbox is controlled. Defaults tofalse.name(:string) - The name of the checkbox input for form submission.form(:string) - The form id to associate the checkbox with.aria_label(:string) - The accessible label for the checkbox. Defaults to"Label".disabled(:boolean) - Whether the checkbox is disabled. Defaults tofalse.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".read_only(:boolean) - Whether the checkbox is read-only. Defaults tofalse.invalid(:boolean) - Whether the checkbox has validation errors. Defaults tofalse.required(:boolean) - Whether the checkbox is required. Defaults tofalse.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[].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.- 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
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