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:

Manual Slot

<.accordion>
  <:trigger value="anatomy">Anatomy</:trigger>
  <:trigger value="machine">State machines</:trigger>
  <:content value="anatomy">Structure & slots</:content>
  <:content value="machine">Zag.js on the client</:content>
</.accordion>

With List

<.accordion
  class="accordion"
  items={
Corex.Content.new([
  %{trigger: "Anatomy", content: "Structure & slots"},
  %{trigger: "State machines", content: "Zag.js on the client"}
])
  }
/>

API Control and Events

Control components from client or server:
Client

<.action phx-click={Corex.Accordion.set_value("my-accordion", ["item-1"])}>
  Open Item 1
</.action>

Server

def handle_event("open_item", _, socket) do
  {:noreply, Corex.Accordion.set_value(socket, "my-accordion", ["item-1"])}
end

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

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.

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.

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

Documentation

Corex Demo
Corex Hex Doc
Corex Hex PM
Github:

https://github.com/corex-ui/corex

23 Likes