Cinder UI

CI

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_app

1. 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"
    ]
  ]
end

Install 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"},
    # ...
  ]
end

Fetch dependencies:

mix deps.get

3. Run the installer

Cinder UI includes a Mix task that sets up CSS, JavaScript hooks, and Tailwind plugins automatically:

mix cinder_ui.install

This will:

The installer auto-detects your package manager (npm, pnpm, yarn, or bun). To specify one explicitly:

mix cinder_ui.install --package-manager pnpm

To 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-patching

4. 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
end

Existing 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.Forms

5. Start building

Start your Phoenix server:

mix phx.server
# or from the repo root:
./bin/demo --port 4001

Try 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:

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:

Supported commands depend on the component, but the common baseline is:

Some input-style components also support:

Current limitation:

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.

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 docs

Feasibility 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