Cinder UI
shadcn/ui components for Phoenix + LiveView.
Cinder UI is a Hex-oriented component library that ports shadcn/ui design patterns, classes, tokens, and compositional structure into Elixir function components.
Installation
Prerequisites
You need an existing Phoenix 1.7+ project. If you don't have one yet:
mix phx.new my_app
cd my_app1. Set up Tailwind CSS
Cinder UI requires Tailwind CSS v4+. New Phoenix projects generated with mix phx.new include Tailwind by default — if yours already has it, skip to step 2.
Add the Tailwind plugin to your dependencies in mix.exs:
defp deps do
[
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
# ...
]
end
Configure Tailwind in config/config.exs:
config :tailwind,
version: "4.1.12",
my_app: [
args: ~w(
--input=assets/css/app.css
--output=priv/static/assets/app.css
),
cd: Path.expand("..", __DIR__)
]
Add the Tailwind watcher in config/dev.exs:
config :my_app, MyAppWeb.Endpoint,
watchers: [
tailwind: {Tailwind, :install_and_run, [:my_app, ~w(--watch)]}
]
Add Tailwind to the deployment alias in mix.exs:
defp aliases do
[
"assets.deploy": [
"tailwind my_app --minify",
"esbuild my_app --minify",
"phx.digest"
]
]
endInstall Tailwind and fetch dependencies:
mix deps.get
mix tailwind.install
Set up assets/css/app.css:
@import "tailwindcss";
If your assets/js/app.js imports CSS (import "../css/app.css"), remove that line — Tailwind handles CSS compilation separately.
2. Add Cinder UI
Add the dependency to your mix.exs:
defp deps do
[
{:cinder_ui, "~> 0.1.0"},
# Optional but recommended — required for the <.icon /> component
{:lucide_icons, "~> 2.0"},
# ...
]
endFetch dependencies:
mix deps.get3. Run the installer
Cinder UI includes a Mix task that sets up CSS, JavaScript hooks, and Tailwind plugins automatically:
mix cinder_ui.installThis will:
-
Copy
cinder_ui.cssintoassets/css/(theme variables and dark mode) -
Copy
cinder_ui.jsintoassets/js/(LiveView hooks for interactive components) -
Update
assets/css/app.csswith:@source "../../deps/cinder_ui";— so Tailwind scans component classes@import "./cinder_ui.css";— loads theme tokens
-
Update
assets/js/app.jsto mergeCinderUIHooksinto your LiveView hooks -
Install the
tailwindcss-animatenpm package
The installer auto-detects your package manager (npm, pnpm, yarn, or bun). To specify one explicitly:
mix cinder_ui.install --package-manager pnpmTo re-run without overwriting customized files:
mix cinder_ui.install --skip-existing
To only (re)copy cinder_ui.css and cinder_ui.js without patching app.css/app.js:
mix cinder_ui.install --skip-patching4. Configure your app
Add use CinderUI to your app's html_helpers in lib/my_app_web.ex:
defp html_helpers do
quote do
use Phoenix.Component
use CinderUI
# ...
end
endExisting projects with CoreComponents
Phoenix generates a CoreComponents module with functions like button/1, input/1, table/1, flash/1, card/1, and label/1 that overlap with Cinder UI components. Cinder UI provides replacements for all of these.
Recommended: Remove conflicting CoreComponents
The cleanest approach is to delete the overlapping functions (button/1, card/1, flash/1, flash_group/1, input/1, label/1, table/1, and icon/1) from your CoreComponents module and let Cinder UI's versions take over. This gives you a single, consistent component API with no namespace prefixes needed. If your CoreComponents only contains the default Phoenix-generated functions, you can remove the module entirely and replace import MyAppWeb.CoreComponents with use CinderUI in your html_helpers.
If you'd prefer to keep your existing CoreComponents alongside Cinder UI, you have several options:
Option A: Exclude conflicting components
Use the except option to skip components that conflict with your CoreComponents:
use CinderUI, except: [:button, :card, :flash, :flash_group, :input, :label, :table]
Excluded components are still available via the CinderUI.UI facade (see Option B) or
their full module path (e.g., <CinderUI.Components.Actions.button>).
Option B: Use the CinderUI.UI facade
Alias the facade module for zero-conflict namespaced access to every component:
alias CinderUI.UI
Then use components with the UI. prefix:
<UI.button>Click me</UI.button>
<UI.autocomplete id="search" name="q" value="">
<:option value="foo" label="Foo" />
</UI.autocomplete>
<UI.icon name="chevron-down" class="size-4" />
This can be combined with Option A — use except for the components that conflict,
and access excluded components via UI.button, UI.input, etc.
Option C: Selectively import specific modules
import CinderUI.Components.Actions
import CinderUI.Components.Forms5. Start building
Start your Phoenix server:
mix phx.server
# or from the repo root:
./bin/demo --port 4001Try a component in any template:
<.button>Click me</.button>Forms and Validation
CinderUI.Components.Forms supports both simple field wrappers and more explicit
field composition for validated LiveView forms.
Basic field usage:
<.field>
<:label><.label for="project-name">Project name</.label></:label>
<.input id="project-name" name="project[name]" />
<:description>Visible to your team in dashboards and alerts.</:description>
</.field>Explicit composition with validation messaging:
<.form for={@form} phx-change="validate" phx-submit="save" class="space-y-6">
<.field invalid={@form[:owner].errors != []}>
<:label>
<.label for={@form[:owner].id}>Owner</.label>
</:label>
<.field_control>
<.autocomplete
id={@form[:owner].id}
name={@form[:owner].name}
value={@form[:owner].value}
aria-label="Owner"
>
<:option value="levi" label="Levi Buzolic" description="Engineering" />
<:option value="mira" label="Mira Chen" description="Design" />
<:empty>No matching teammates.</:empty>
</.autocomplete>
</.field_control>
<.field_description>Pick the teammate responsible for the workspace.</.field_description>
<.field_error :for={{msg, _opts} <- @form[:owner].errors}>{msg}</.field_error>
</.field>
<.button type="submit">Save</.button>
</.form>Available field helpers:
field/1field_label/1field_control/1field_description/1field_message/1field_error/1input/1select/1native_select/1autocomplete/1
Interactive Commands
Interactive components that ship with Cinder UI hooks now share a small command
surface through the cinder-ui:command custom event. You can drive that
surface directly from LiveView with CinderUI.JS.
For overlay-style components that use the shipped hooks, the current baseline behavior is:
Escapecloses dialogs, drawers, popovers, and dropdown menus- outside click closes popovers and dropdown menus
- dialog and drawer overlay clicks dismiss the overlay
- hook bindings are refreshed after LiveView-driven DOM updates
Supported commands depend on the component, but the common baseline is:
openclosetogglefocus
Some input-style components also support:
clear
Current limitation:
-
popover and dropdown content still use fixed offset positioning classes such
as
mt-2; viewport-aware flipping and collision handling are not implemented yet
LiveView example:
<button phx-click={CinderUI.JS.open(to: "#account-dialog")}>
Open dialog
</button>
<button phx-click={CinderUI.JS.clear(to: "#owner-autocomplete")}>
Clear owner
</button>Raw event example:
const dialog = document.querySelector("[data-slot='dialog']")
dialog?.dispatchEvent(
new CustomEvent("cinder-ui:command", {
detail: { command: "open" },
}),
)
If you import CinderUI from assets/js/cinder_ui.js, you can also dispatch
through the helper:
import { CinderUI, CinderUIHooks } from "./cinder_ui"
CinderUI.dispatchCommand(document.querySelector("[data-slot='select']"), "toggle")Icons (Optional, Recommended)
CinderUI.Icons.icon/1 dispatches to lucide_icons.
-
Cinder UI reads
lucide_icons.icon_names/0and caches names automatically. - Both kebab-case and snake_case icon names are supported.
Example:
<.icon name="chevron-down" class="size-4" />
<.icon name="loader_circle" class="size-4 animate-spin" />
If lucide_icons is missing and <.icon /> is used, Cinder UI raises an error.
Theming and Style Overrides
Cinder UI uses shadcn-style CSS variables (--background, --foreground, --primary, etc.) and dark mode with .dark.
Configure variables (shadcn-style)
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.54 0.22 262);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--radius: 0.75rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.72 0.18 262);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
}
Set your preferred corner scale by changing --radius; component classes (rounded-md, rounded-lg, etc.) derive from that value through the Tailwind token mapping in assets/css/cinder_ui.css.
API Docs
The docs site is the source of truth for component coverage, examples, and current behavior. Every component module also includes in-source docs and usage examples. Generate docs with:
mix docsFeasibility Notes
A subset of shadcn components rely on browser-first stacks (Radix primitives, complex keyboard navigation, chart engines, or heavy client state). For these, Cinder UI provides either progressive LiveView hook behavior or a scaffold component with stable API + styling.
Attribution and Third-Party Notices
Cinder UI is built on the shoulders of giants, leveraging the awesome work from these projects:
Thank you to the maintainers and contributors.
For third-party license details and links to upstream license texts, see: THIRD_PARTY_NOTICES.md
Contributing
See CONTRIBUTING.md for contributor setup, quality gates, testing, release workflow, and docs/site build maintenance.
License
MIT