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)
- Complete all components (Phoenix attributes, initial rendering, props)
- Complete API (programmatic control methods for each component)
- Complete documentation (usage examples, styling guides, accessibility notes)
- Mix tests (including Connect API and state synchronization)
- E2E tests (including accessibility testing with axe-core)
- Mix generator for
phx.new(Phoenix installer fork with Corex) - Mix generator template for
phx.gen(generate LiveView pages using Corex) - Playground/Storybook (interactive documentation)
- 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:


























