Corex - Accessible and unstyled UI Phoenix components

Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and LiveView hooks.
It works with both Phoenix Controllers and LiveView without requiring a JavaScript framework or Node.js build process.

Currently in early alpha, looking for feedback on the architecture, API design, and overall approach

History

I originally created corex-ui.com, a Vanilla JS integration of Zag.js for static websites. The challenge was adapting this approach to Phoenix’s server-rendered model while feeling natural to Phoenix developers. Corex is the result: interactive, accessible components that work with Phoenix conventions rather than against them.

Why Corex

State Machines for Complex Interactions

Zag.js handles intricate state management and accessibility concerns. An accordion must manage which items are open/closed, keyboard navigation, focus management, ARIA attributes, and animation states. Rather than implementing this yourself, Zag.js provides battle-tested state machines.

Seamless Phoenix Integration

Corex wraps Zag.js with ergonomic Phoenix components:

<.accordion class="accordion">
  <:item :let={item}>
    <.accordion_trigger item={item}>
      Lorem ipsum dolor sit amet
    </.accordion_trigger>
    <.accordion_content item={item}>
      Consectetur adipiscing elit...
    </.accordion_content>
  </:item>
</.accordion>

API Control

Control components from client or server:

<button phx-click={Corex.Accordion.set_value("my-accordion", ["item-1"])}>
  Open Item 1
</button>
def handle_event("open_item", _, socket) do
  {:noreply, Corex.Accordion.set_value(socket, "my-accordion", ["item-1"])}
end

Customization Over Configuration

Corex avoids structural configuration. All structure is expressed through slots and nested components, never inferred from attributes. The accordion trigger uses a nested component rather than a text attribute:

<.accordion_trigger item={item}>
   Lorem ipsum dolor sit amet
    <:trigger>
    <.icon name="hero-chevron-down" />
    </:trigger>
</.accordion_trigger>

This requires more code than title="..." but ensures components remain unstyled, composable, and adaptable without fighting constraints.

Unstyled by Default

Components ship with zero styling. They expose semantic data attributes you can target with your own CSS:

[data-scope="accordion"][data-part="item-trigger"] {
  /* Your styles */
}

[data-scope="accordion"][data-part="item-trigger"][data-state="open"] {
  /* Open state styles */
}

Works with any design system without style overrides or specificity battles.

Simple by Design

Installation is straightforward:

use Corex
import Hooks from "corex"

const liveSocket = new LiveSocket("/live", Socket, {
  hooks: {...colocatedHooks, ...Hooks}
})

The TypeScript integration is pre-compiled and shipped with the Hex package. You work entirely within Elixir’s toolchain: mix deps.get, configure hooks, start building.

Progressive Enhancement

Uncontrolled by default: Components manage their own state on the client using Zag.js. User interactions update the UI immediately without server round-trips. Covers most use cases.

<.accordion class="accordion">
  <:item :let={item}>
    <.accordion_trigger item={item}>Click me</.accordion_trigger>
    <.accordion_content item={item}>Content here</.accordion_content>
  </:item>
</.accordion>

Controlled when needed: The server owns the state. State changes emit as events and reflect back through assigns. Useful when component state must be validated, persisted, or coordinated with application logic.

def mount(_params, _session, socket) do
  {:ok, assign(socket, :value, ["item-1"])}
end

def handle_event("on_value_change", %{"value" => value}, socket) do
  {:noreply, assign(socket, :value, value)}
end

Both modes expose the same interaction API and can be mixed within the same application.

Forms and Validation

Integrates with Phoenix forms without custom abstractions. Components work without server validation (client-managed state) or with changesets (server-side validation). Form fields, labels, and errors are passed explicitly through slots.

Roadmap (Priority Order)

  1. Complete all components (Phoenix attributes, initial rendering, props)
  2. Complete API (programmatic control methods for each component)
  3. Complete documentation (usage examples, styling guides, accessibility notes)
  4. Mix tests (including Connect API and state synchronization)
  5. E2E tests (including accessibility testing with axe-core)
  6. Mix generator for phx.new (Phoenix installer fork with Corex)
  7. Mix generator template for phx.gen (generate LiveView pages using Corex)
  8. Playground/Storybook (interactive documentation)
  9. Low-level API (expose Connect module for custom components)

Feedback, and suggestions welcome as I continue developing this library.

Documentation

Corex Demo
Corex Hex Doc
Corex Hex PM
Github:

20 Likes

Looking great, congrats on the launch!

1 Like

Thanks for doing this!

I have been hoping to see something based on ZagJS for a while and I’m very thankful you’ve taken this on. I’m excited to see this grow :blush:

1 Like

Do you have plans for 3-state switch? That’s a feature which is often missed, but very helpful.

Do you support / or plan to support “native” HTML5 + CSS3 solutions (that does not require JavaScript at all). See below link for an accordion example:

A huge :+1: for making components accessible by default - it’s a huge help for developers!

There is currently no plan to have other interactive component than Zagjs
Mainly because they come packed with accessibility features that would be hard to replicate and test
Let’s take an example:
The accordion has Direction (LTR/RTL) and an orientation (Vertical/Horizontal).
Some items may be disabled
The user can navigate from one Item to the previous/next with keyboard arrows.
The logic will change depending on the combinaison of all props and will skip the disabled items

You can see an example in the e2e app on the Accordion Playground /playground/accordion

Thank you for the support

1 Like

Phoenix Form Support

Corex now supports Phoenix forms for Checkbox, Select and Switch components.

You can simply pass a Phoenix form field to the components

  • In controller views, the client is the source of truth.
  • In LiveView, the server remains the source of truth and components sync accordingly
  • Validation errors stay fully synchronized and are fully customizable.
  • Error rendering follows Phoenix’s native input interaction model, preventing error messages from appearing on fields the user has not interacted with.
  • Get error translations with custom Gettext Backend

All components pass Phoenix.LiveViewTest, making them safe drop-in replacements for components generated by Phoenix generators.

The E2E app includes both controller and LiveView form examples generated with:

  • mix phx.gen.html
  • mix phx.gen.live

Read Documentation


Phoenix Flash Support

The Toast component now supports the native Phoenix Flash system.

  • Pass flash={@flash} to toast_group and use flash messages as usual.
  • New components are introduced for the following lifecycle events:
    • phx-connected
    • phx-disconnected
    • client-error
    • server-error
  • You can define custom toasts for these four lifecycle events anywhere in your application.

Toast creation is now more flexible, with a loading option available for all toast types.

More details:


Accordion List Support

Thanks to the advise from @achempion, Accordion now supports loading items from a list via Accordion.List.Item.

This feature uses the unified Corex.Collection.Item structure, which includes an additional meta field for attaching custom, dynamic data to each item.

<.accordion
  class="accordion"
  items={[
    %Corex.Accordion.Item{
      value: "lorem",
      trigger: "Lorem ipsum dolor sit amet",
      content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique.",
      meta: %{
        indicator: "hero-chevron-right",
      }
    },
    %Corex.Accordion.Item{
      trigger: "Duis dictum gravida odio ac pharetra?",
      content: "Nullam eget vestibulum ligula, at interdum tellus.",
      meta: %{
        indicator: "hero-chevron-right",
      }
    },
    %Corex.Accordion.Item{
      value: "donec",
      trigger: "Donec condimentum ex mi",
      content: "Congue molestie ipsum gravida a. Sed ac eros luctus.",
      disabled: true,
      meta: %{
        indicator: "hero-chevron-right",
      }
    }
  ]}
>
  <:item :let={item}>
    <.accordion_trigger item={item}>
      {item.meta.trigger}
      <:indicator>
        <.icon name={item.meta.indicator} />
      </:indicator>
    </.accordion_trigger>

    <.accordion_content item={item}>
      {item.meta.content}
    </.accordion_content>
  </:item>
</.accordion>

See more Accordion examples

Select and Combobox Enhancements

Select and Combobox components now support:

  • Grouping
  • Disabling items
  • Per-item customization using dynamic data

These components rely on the unified Corex.Collection.Item structure with a meta field for extensibility.

Corex.Select — Corex v0.1.0-alpha.11
Corex.Combobox — Corex v0.1.0-alpha.11

Design Tokens

Integrate design tokens into Tailwind CSS v4 using Style Dictionary and Tokens Studio, entirely from Elixir, with no Node.js dependency.

Use the mix corex.design mix task to generate and manage design tokens. Full instructions are available in the documentation

3 Likes

New components

Five new components are available:

  • Clipboard Copy text to the clipboard with configurable trigger and copied states

  • Collapsible Show/hide content with expand/collapse

  • Date Picker Date selection with calendar view and keyboard navigation

  • Dialog Modal dialogs with focus management and accessibility support

  • Tabs Tabbed interfaces for organizing content

Unified list item

Accordion and Tabs now share a common %Corex.List.Item{} struct. Use it for programmatic item lists:

%Corex.List.Item{

  value: "item-1",

  trigger: "Section title",

  content: "Section content",

  meta: %{indicator: "hero-chevron-right"}

}

Form integration

Phoenix Form docs have been expanded for Checkbox, Date Picker, Select, and Switch, including:

  • Controller usage

  • LiveView usage with controlled mode

  • Ecto changeset examples

See each component’s documentation for examples.

Feedback is always welcome. Happy coding

2 Likes

Dark Mode Toggle

Built on Corex.ToggleGroup, it uses a triple-layer approach (cookies + localStorage + immediate script execution) to ensure:

  • No FOUC (Flash of Unstyled Content)
  • Syncs across browser tabs
  • Respects system preferences
  • Works perfectly with LiveView and controllers

Signature Pad

  • Full Phoenix form integration (controllers & LiveView)
  • Works with and without Ecto changesets
  • Controlled/uncontrolled modes
  • Customizable drawing options (color, size, pressure simulation)

Signature Pad joins the growing collection of form components (checkbox, select, date picker) that work seamlessly in both traditional controllers and LiveView, with or without Ecto Changeset

Happy coding

3 Likes

Corex Demo is live

Thanks to Gigalixir to offer a great hosting experience for elixir and Phoenix
Try Corex in production with some example for each components, a Phoenix form in a controller and in a Live View, language switch, dark mode and site navigation

Language Switch and RTL

Using the select component, Plugs and cookies, Corex guide you to implement your own local system using Gettext. RTL support is also available to synchronize all components

Tree View

New component added to display a tree of data with the ability to use it as a navigation. Works in controller and Live View

Feedback is always welcome. Happy coding

Tree view live produces an internal server error on the demo site.

Thanks for trying out. TreeView Live View page is fixed now.

On the same update I also improved the code splitting and lazy loading of the components.
Only loads the components in use on the current page.
Check out the new “Performance” section on the Installation guide
The chunks we see are the shared parts between the components, so no duplication.