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).

FunctionActionReturns
set_checked/2Set checked state (client)%Phoenix.LiveView.JS{}
set_checked/3Set checked state (server)socket
toggle_checked/1Toggle checked state (client)%Phoenix.LiveView.JS{}
toggle_checked/2Toggle 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)}
end

Events

User-driven only. Declarative checked may be true, false, or :indeterminate; imperative set_checked is always boolean.

Server events

EventWhenPayload
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)}
end

Client events

EventWhenevent.detail
on_checked_change_client="checkbox-changed"User toggles checked stateid, 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)}
end

Style

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-smui-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.

ModifierClasses
Defaultcheckbox
Accentcheckbox ui-accent
Brandcheckbox ui-brand
Alertcheckbox ui-alert
Infocheckbox ui-info
Successcheckbox 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

ModifierClasses
SMcheckbox ui-size-sm
Defaultcheckbox
LGcheckbox ui-size-lg
XLcheckbox 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
    end

Ecto

    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

For more form patterns (controller, LiveView, Ecto validation), see the Forms guide.

Summary

Components

Renders a checkbox component.

Renders a loading skeleton for the checkbox component.

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

checkbox(assigns)

Renders a checkbox component.

Attributes

  • id (:string) - The id of the checkbox, useful for API to identify the checkbox. Defaults to nil.
  • 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 to nil.
  • name (:string) - The name of the checkbox input for form submission. Defaults to nil.
  • form (:string) - The form id to associate the checkbox with. Defaults to nil.
  • invalid (:boolean) - Whether the control has validation errors. Defaults to nil.
  • auto_invalid (:boolean) - When true with field, set invalid from visible changeset errors (default false). Defaults to false.
  • controlled (:boolean) - Whether the control is controlled. Defaults to false.
  • disabled (:boolean) - Whether the control is disabled. Defaults to false.
  • read_only (:boolean) - Whether the control is read-only. Defaults to false.
  • required (:boolean) - Whether the control is required. Defaults to false.
  • checked (:any) - Checked state: true, false, or :indeterminate (Zag CheckedState). Form fields still use boolean. Defaults to false.
  • 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 to nil. Must be one of nil, "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_event receives %{"id" => id, "checked" => boolean}. Defaults to nil.
  • on_checked_change_client (:string) - Browser event type on the checkbox element when checked changes. event.detail: { id, checked }. Defaults to nil.
  • 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)

checkbox_skeleton(assigns)

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 to true.
  • dir (:string) - Same as checkbox: logical direction for layout. Defaults to nil. Must be one of nil, "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(checkbox_id, checked)

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(socket, checkbox_id, checked)

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_many(socket, checkbox_ids, checked)

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(checkbox_id)

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(socket, checkbox_id)

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