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:
































