Manual installation
View SourceThis guide is the Phoenix wiring home for Corex in an existing app: dependency, ESM Esbuild, hooks, root layout module script, use Corex, plus optional Design, Theme, Mode, Accessibility, and Locale plumbing (plugs, config, bridge scripts, lang/dir, and related hooks).
Picker UI (theme select, mode toggle, language switcher, accessibility panel) lives in the dedicated guides after you finish the wiring here:
If you are creating a new project instead, see the Installation guide.
Requirements
- Elixir
~> 1.17 - Phoenix and LiveView
- A standard Esbuild asset pipeline
1. Add the dependency
Add corex to your mix.exs deps:
def deps do
[
{:corex, "~> 0.2.0"}
]
endThen fetch the dependencies:
mix deps.get
2. Esbuild
Corex's JavaScript ships as ECMAScript modules with dynamic import(). Each component hook loads its own chunk on demand, so a component that never appears on a page is never fetched.
This requires two Esbuild flags on your main app target: --format=esm, --splitting and --outdir=../priv/static/assets/js. In config/config.exs:
config :esbuild,
version: "0.25.12",
my_app: [
args:
~w(js/app.js --bundle --format=esm --splitting --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]3. Phoenix Hooks
All Corex hooks
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import corex from "corex"
const csrfToken = document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: { ...corex },
})
liveSocket.connect()Merge with colocatedHooks when your app uses them:
hooks: { ...colocatedHooks, ...corex },Eager chrome + lazy extras
Static-import chrome that exists on every page (toast, theme/language select, mode toggle, accessibility panel). Keep page-local components lazy. Keep longPollFallbackMs for real LiveView apps; do not call disableDebug() (Tableau sites do that separately).
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import { hooks } from "corex/hooks"
import { Toast } from "corex/toast"
import { Select } from "corex/select"
import { Toggle } from "corex/toggle"
import { Dialog } from "corex/dialog"
import { ToggleGroup } from "corex/toggle-group"
const csrfToken = document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: {
Toast,
Select,
Toggle,
Dialog,
ToggleGroup,
...hooks({
Accordion: () => import("corex/accordion"),
Combobox: () => import("corex/combobox"),
}),
},
})
liveSocket.connect()Omit chrome imports you do not render in the root layout. Merge with colocated hooks:
hooks: {
...colocatedHooks,
Toast,
Select,
Toggle,
...hooks({
Accordion: () => import("corex/accordion"),
}),
},Lazy hooks only
Import only the hooks you render. Keys must match phx-hook names (Dialog, Accordion, …). Prefer the eager-chrome tab above when theme, mode, language, accessibility, or toast live in the root layout; lazy chrome waits on a second chunk before those controls respond.
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import { hooks } from "corex/hooks"
const csrfToken = document
.querySelector("meta[name='csrf-token']")
?.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: { _csrf_token: csrfToken },
hooks: {
...hooks({
Accordion: () => import("corex/accordion"),
Dialog: () => import("corex/dialog"),
Combobox: () => import("corex/combobox"),
}),
},
})
liveSocket.connect()Each value is a zero-argument function returning a dynamic import(). Esbuild emits chunks only for listed hooks.
4. Root layout: load app.js as a module
The Corex JS bundle is ESM, so the browser must load it as a module. In lib/my_app_web/components/layouts/root.html.heex, set type="module" on the <script> tag that loads assets/js/app.js:
<script defer phx-track-static type="module" src={~p"/assets/js/app.js"}></script>If your root layout already uses type="text/javascript" (the phx.new default), replace text/javascript with module. If it has no type at all, add type="module" next to phx-track-static.
5. Import Corex
In your web module (typically lib/my_app_web.ex), add use Corex inside the quote block of defp html_helpers, alongside the other imports that apply to HEEx templates:
defp html_helpers do
quote do
use Gettext, backend: MyAppWeb.Gettext
import Phoenix.HTML
use Corex
alias Phoenix.LiveView.JS
alias MyAppWeb.Layouts
unquote(verified_routes())
end
endDo not keep Phoenix CoreComponents in a Corex app. Use Corex components and scaffold with mix corex.gen.html / mix corex.gen.live instead of mix phx.gen.*.
By default this imports every Corex function component (accordion/1, combobox/1, dialog/1, …). If you want a smaller surface area or to avoid name collisions with other components, narrow it with only: / except: and an optional prefix::
use Corex, only: [:accordion], prefix: "ui"<.ui_accordion
id="my-accordion"
class="accordion"
items={Corex.Content.new([
[value: "first", label: "First", content: "First panel."],
[value: "second", label: "Second", content: "Second panel."],
[value: "third", label: "Third", content: "Third panel."]
])}
/>Compile and rebuild assets:
mix compile
mix assets.build
6. Optional: Corex Design
Add the corex_design dependency to mix.exs:
{:corex_design, "~> 0.2", runtime: false, only: :dev},Optionally rebuild Design CSS on every compile (most apps call the build from assets.build / assets.deploy instead):
def project do
[
compilers: Mix.compilers() ++ [:corex_design]
]
endAdd to config/config.exs (build-time CSS only; see Configuration):
config :corex_design,
output: "assets/corex",
default_theme: :neo,
default_mode: :light,
themes: nil,
scales: [],
components: ~w(button dialog accordion typo layout-heading)a,
semantics: nildefault_theme / default_mode / themes control which theme CSS the design build emits. They are not the runtime picker allowlist (config :my_app, :themes). components: lists the component recipes to emit. Omit the key or set nil for the full catalog. semantics: trims unused palette roles and ui-{role} utilities when you need a smaller bundle. List allowed keys with mix corex.design.options.
Add "corex.design.build" to your assets.build and assets.deploy aliases in mix.exs.
Ignore the generated output in git (rebuild with mix corex.design.build):
/assets/corex/If that tree was already committed, stop tracking it without deleting files on disk:
git rm -r --cached assets/corex
git commit -m "Stop tracking generated Corex Design CSS"
Generate CSS:
mix deps.get
mix corex.design.build
Then import from assets/css/app.css. Prefer the single umbrella entry:
@import "../corex/corex.css";
@source "../corex";Layered imports (main.css + theme/neo.css + components.css) still work if you filter themes yourself. components.css is generated from the components: list in config :corex_design.
If your app.css still imports the stock daisyUI plugin from phx.new, remove or isolate it. Mixing daisyUI tokens with Corex Design tokens leads to duplicated reset rules and conflicting CSS variables.
Finally, set data-theme and data-mode on <html> so token files such as theme/neo.css and light/dark palettes apply. Use values that match your imports and toggles (for example data-theme="neo" when you import ../corex/theme/neo.css, and data-mode="light" or data-mode="dark"). Sections 8 and 9 wire these from plugs and bridge scripts; the picker UI is in Theming and Dark mode.
Give <body> the typo class so base typography applies (use Tailwind utilities for page layout):
<html lang="en" data-theme="neo" data-mode="light">
<body class="typo">
{@inner_content}
</body>
</html>See the Design guide for commands, modifiers, bundle filtering, and themes.
7. Optional: Phoenix flash with Toast
To render Phoenix flash (and LiveView flash) as Corex toasts instead of the default <.flash_group>, render a <.toast_group> in your app layout and pass it flash={@flash}. In lib/my_app_web/components/layouts.ex, replace the flash group inside def app/1 with:
<.toast_group id="layout-toast" class="toast" flash={@flash}>
<:loading>
<.heroicon name="hero-arrow-path" />
</:loading>
<:close>
<.heroicon name="hero-x-mark" />
</:close>
</.toast_group>Optionally, add the connection-state toasts so users see feedback when the socket drops or the server errors out:
<.toast_client_error
toast_group_id="layout-toast"
title={gettext("We can't find the internet")}
description={gettext("Attempting to reconnect")}
type={:error}
duration={:infinity}
/>
<.toast_server_error
toast_group_id="layout-toast"
title={gettext("Something went wrong!")}
description={gettext("Attempting to reconnect")}
type={:error}
duration={:infinity}
/>Make sure every LiveView and controller view that uses this layout passes flash={@flash} into it (e.g. <Layouts.app flash={@flash} ...>).
See Corex.Toast for create/5, create/6, update/3, update/4, remove/2, remove/3, and dismiss/2 / dismiss/3. Pass action: %{label: "…", js: %Phoenix.LiveView.JS{}} on server create/6 / update/4 only (client bindings ignore :action). Compose js with JS.push, JS.patch, or JS.navigate.
Optional: Theme wiring
Runtime theme picker allowlist in config/config.exs (first entry is the default):
config :my_app, :themes, ~w(neo uno duo leo)This list is for the picker and plug validation only. Trim emitted CSS with config :corex_design, themes: (build-time). Keep the picker list a subset of the themes you build.
Create lib/my_app_web/plugs/theme.ex that reads phx_theme cookie, validates against :themes, and assigns :theme / :themes. Put plug MyAppWeb.Plugs.Theme in the browser pipeline (after :fetch_live_flash; after locale plugs when you use --lang).
On <html>:
<html lang="en" data-theme={assigns[:theme] || "neo"} data-mode={assigns[:mode] || "light"}>Add the before-paint bridge and register the Select hook, then render the picker UI:
Optional: Mode wiring
Create MyAppWeb.Plugs.Mode that reads phx_mode cookie (light / dark) and assigns :mode. Put it in the browser pipeline with Theme (Mode before Theme is fine).
Ensure root <html> has data-mode={assigns[:mode] || "light"}.
Add the before-paint bridge and register the Toggle hook, then render the toggle UI:
Include toggle in config :corex_design, components: when you use a mode switcher.
Optional: Accessibility wiring
Enable preference CSS in Design, then wire the plug, LiveView assign, root data-* attrs, and FOUC bridge. Full steps (including the panel UI) are in Accessibility.
Short path with the installer:
mix corex.new my_app --a11y
Or by hand:
- Set
config :corex_design, accessibility: true(or an axis list) and rebuild withmix corex.design.build - Add
MyAppWeb.Plugs.Accessibilityto the browser pipeline and a LiveViewon_mountthat assigns:a11y - Apply
a11y_data_attrs/1on<html>and merge thephx:a11ybridge into the same<head>IIFE as theme/mode - Render an accessibility panel once in the root layout; register
DialogandToggleGrouphooks
Scaffolding is also available as mix corex.new --a11y (default off) and mix corex.tableau.new --a11y.
Optional: Locale wiring
Routing and layout wiring for Gettext + Localize. The language switcher UI is in Localize.
- Add
localize_webandgettext_sigils; align Gettextlocales:withconfig :localize, supported_locales:. - Run
mix localize.download_localesafter changing locales. - Set VerifiedRoutes
path_prefixes: [{MyAppWeb.Locale, :current, []}]. use Localize.Routesin the router; put locale plugs immediately after:fetch_live_flash(Mode/Theme plugs afterLocalize.Plug.PutSession).- Implement
MyAppWeb.Localehelpers (lang/0,dir/0,current/0) and root<html lang={…} dir={…}>. - For LiveViews,
on_mounta layout hook so locale andcurrent_pathstay in sync.
mix corex.new my_app --lang scaffolds this shape. Full switcher markup: Localize.
8. Add your first component
After the install, every Corex function component is available in your templates. The id attribute is required for any component you want to drive from the API.
Corex.Content.new/1 builds a list of items. Each item's value is auto-generated when missing; you can also flag an item as disabled.
<.accordion
id="welcome-accordion"
class="accordion"
items={Corex.Content.new([
[label: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique."],
[label: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus."],
[label: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus."]
])}
/>Driving components from the API
Every component documents its helpers under Corex.<Name> in Hexdocs (see API and Events on each module page). You need a stable id on the root.
Client-side (inline binding):
<button type="button" phx-click={Corex.Accordion.set_value("welcome-accordion", ["1"])}>
Open the first panel
</button>Server-side (handle_event/3):
def handle_event("open_first", _params, socket) do
{:noreply, Corex.Accordion.set_value(socket, "welcome-accordion", ["1"])}
endFor custom slots, controlled values, async loading, and the full API, see Corex.Accordion.
What's next
Wiring done? Add the picker UI:
- Theming theme
<.select> - Dark mode mode
<.toggle> - Accessibility preference panel (also
mix corex.new --a11y) - Localize language switcher
Also:
- Design modifiers, bundle filtering, and themes
- Forms
field, validation, andauto_invalid - MCP AI tooling in development (
mix corex.newwrites.cursor/mcp.json; use--no-mcpto skip) - Production prod build and run
- Updating Corex migrate an existing app
Logger parameter filtering
Phoenix only filters params containing "password" by default. Expand the filter for tokens and secrets:
config :phoenix, :filter_parameters, ["password", "secret", "token", "otp", "_key", "api_key"]