# PhiaUI
**Enterprise-ready Phoenix LiveView component library inspired by shadcn/ui.**
Ejectable components with zero heavy JS dependencies, full WAI-ARIA accessibility, and built-in analytics widgets for financial terminals, BI dashboards, and KPI monitors.
[](https://hex.pm/packages/phia_ui)
[](https://elixir-lang.org)
[](LICENSE)
---
## Why PhiaUI?
| Feature | PhiaUI | salad_ui |
|---------|--------|----------|
| Ejectable architecture | Yes | Partial |
| Enterprise analytics widgets | Yes | No |
| Zero npm deps for interactivity | Yes | No |
| Native ClassMerger (no tw_merge) | Yes | No |
| TailwindCSS v4 theme system | Yes | No |
| WAI-ARIA on all interactive | Yes | Partial |
---
## Implemented Components
### Primitives (Stateless, no JS)
| Component | Function | Sub-components |
|-----------|----------|----------------|
| Button | `button/1` | — |
| Card | `card/1` | `card_header`, `card_title`, `card_description`, `card_content`, `card_footer` |
| Badge | `badge/1` | — |
| Table | `table/1` | `table_header`, `table_body`, `table_footer`, `table_row`, `table_head`, `table_cell`, `table_caption` |
| Icon | `icon/1` | — |
### Form Integration
| Component | Function | Description |
|-----------|----------|-------------|
| Input | `phia_input/1` | Label + input + description + errors, integrated with `Phoenix.HTML.FormField` |
| Textarea | `phia_textarea/1` | Multi-line textarea with form integration |
| Select | `phia_select/1` | Native select with FormField integration |
| Form | `form_field/1`, `form_label/1`, `form_message/1` | Composable form primitives |
| Tags Input | `tags_input/1` | Multi-tag input with deduplication |
| Image Upload | `image_upload/1` | Drop zone + preview, uses native Phoenix LiveView uploads |
| Rich Text Editor | `rich_text_editor/1` | WYSIWYG editor, toolbar with 14 commands, zero npm deps |
### Interactive (JS Hooks)
| Component | Function | Hook |
|-----------|----------|------|
| Dialog | `dialog/1` | `PhiaDialog` |
| Dropdown Menu | `dropdown_menu/1` | `PhiaDropdownMenu` |
| Accordion | `accordion/1` | `Phoenix.LiveView.JS` only |
### Dashboard Shell
| Component | Function | Description |
|-----------|----------|-------------|
| Shell | `shell/1` | CSS Grid desktop layout (sidebar 240px + 1fr) |
| Sidebar | `sidebar/1` | Fixed sidebar with brand, nav, footer slots |
| Sidebar Item | `sidebar_item/1` | Navigation item with active state |
| Topbar | `topbar/1` | Full-width header bar |
| Mobile Sidebar Toggle | `mobile_sidebar_toggle/1` | Hamburger trigger (hidden on md+) |
### Dashboard Widgets
| Component | Function | Description |
|-----------|----------|-------------|
| Stat Card | `stat_card/1` | KPI card with trend indicator (up/down/neutral) |
| Metric Grid | `metric_grid/1` | Responsive grid layout (1–4 cols) |
| Chart Shell | `chart_shell/1` | Titled card wrapper for any chart library |
---
## Installation
Add to `mix.exs`:
```elixir
def deps do
[
{:phia_ui, "~> 0.1.0"}
]
end
```
Run the installer:
```bash
mix phia.install
```
This copies the TailwindCSS v4 theme and the ClassMerger into your project.
### TailwindCSS v4 Theme
In your `assets/css/app.css`:
```css
@import "tailwindcss";
@import "../../../deps/phia_ui/priv/static/theme.css";
```
Or after ejection:
```css
@import "tailwindcss";
@import "./phia_theme.css";
```
The theme provides semantic design tokens:
```css
@theme {
--color-primary: oklch(0.21 0.006 285.885);
--color-primary-foreground: oklch(0.985 0 0);
--color-destructive: oklch(0.577 0.245 27.325);
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
/* ... */
}
```
---
## Mix Tasks
```bash
# Install core dependencies and setup
mix phia.install
# List all available components
mix phia.list
# Eject specific components into your codebase
mix phia.add button card badge
# Generate the Lucide SVG sprite
mix phia.icons
```
---
## Usage Examples
### Button
```heex
<.button>Click me</.button>
<.button variant="destructive">Delete</.button>
<.button variant="outline" size="sm">Small outline</.button>
<.button variant="ghost" disabled>Disabled</.button>
```
Variants: `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`
Sizes: `default`, `sm`, `lg`, `icon`
### Card Composition
```heex
<.card>
<.card_header>
<.card_title>Revenue</.card_title>
<.card_description>Last 30 days</.card_description>
</.card_header>
<.card_content>
<p class="text-3xl font-bold">$48,290</p>
</.card_content>
<.card_footer>
<.badge variant="secondary">+12.5%</.badge>
</.card_footer>
</.card>
```
### Badge
```heex
<.badge>Default</.badge>
<.badge variant="destructive">Error</.badge>
<.badge variant="outline">Pending</.badge>
```
### Icon
```heex
<.icon name="check" />
<.icon name="alert-triangle" size="lg" class="text-destructive" />
```
Requires the Lucide sprite at `/priv/static/icons/lucide-sprite.svg`. Generate with `mix phia.icons`.
### Table with LiveView Streams
```heex
<.table>
<.table_header>
<.table_row>
<.table_head>Name</.table_head>
<.table_head>Status</.table_head>
<.table_head class="text-right">Amount</.table_head>
</.table_row>
</.table_header>
<.table_body>
<.table_row :for={{dom_id, row} <- @streams.rows} id={dom_id}>
<.table_cell><%= row.name %></.table_cell>
<.table_cell><.badge><%= row.status %></.badge></.table_cell>
<.table_cell class="text-right"><%= row.amount %></.table_cell>
</.table_row>
</.table_body>
</.table>
```
### Form with Changeset
```heex
<.form for={@form} phx-change="validate" phx-submit="save">
<.phia_input field={@form[:name]} label="Full Name" />
<.phia_input field={@form[:email]} type="email" label="Email" description="We won't spam you." />
<.phia_textarea field={@form[:bio]} label="Bio" rows={4} />
<.phia_select field={@form[:role]} options={["admin", "editor", "viewer"]} label="Role" />
<.button type="submit">Save</.button>
</.form>
```
### Tags Input
```heex
<.tags_input
field={@form[:tags]}
label="Tags"
placeholder="Add a tag..."
separator=","
/>
```
Tags are stored as a comma-separated string in the hidden input. Requires the `PhiaTagsInput` JS hook.
### Image Upload
```heex
<.live_file_input upload={@uploads.avatar} class="sr-only" />
<.image_upload upload={@uploads.avatar} label="Profile photo" />
```
In your LiveView:
```elixir
def mount(_params, _session, socket) do
{:ok, allow_upload(socket, :avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end
```
### Rich Text Editor
```heex
<.rich_text_editor
field={@form[:content]}
label="Content"
placeholder="Start writing..."
min_height="200px"
/>
```
Requires the `PhiaRichTextEditor` JS hook. Toolbar includes: bold, italic, underline, strikethrough, H1–H3, paragraph, bullet list, ordered list, blockquote, inline code, code block, link.
### Dialog
```heex
<.dialog id="confirm-dialog">
<.dialog_trigger>
<.button variant="outline">Open dialog</.button>
</.dialog_trigger>
<.dialog_content>
<.dialog_header>
<.dialog_title>Confirm action</.dialog_title>
<.dialog_description>This cannot be undone.</.dialog_description>
</.dialog_header>
<.dialog_footer>
<.dialog_close><.button variant="outline">Cancel</.button></.dialog_close>
<.button phx-click="confirm">Confirm</.button>
</.dialog_footer>
</.dialog_content>
</.dialog>
```
Requires the `PhiaDialog` JS hook. Features: focus trap, Escape key, scroll locking, auto-focus.
### Dropdown Menu
```heex
<.dropdown_menu id="actions-menu">
<.dropdown_menu_trigger>
<.button variant="ghost" size="icon"><.icon name="more-horizontal" /></.button>
</.dropdown_menu_trigger>
<.dropdown_menu_content>
<.dropdown_menu_label>Actions</.dropdown_menu_label>
<.dropdown_menu_separator />
<.dropdown_menu_item phx-click="edit">Edit</.dropdown_menu_item>
<.dropdown_menu_item phx-click="delete" class="text-destructive">Delete</.dropdown_menu_item>
</.dropdown_menu_content>
</.dropdown_menu>
```
Requires the `PhiaDropdownMenu` JS hook. Features: smart auto-flip positioning, click-outside detection, keyboard navigation.
### Accordion
```heex
<.accordion type="single">
<.accordion_item accordion_id="item-1">
<.accordion_trigger accordion_id="item-1">What is PhiaUI?</.accordion_trigger>
<.accordion_content accordion_id="item-1">
A Phoenix LiveView component library for enterprise dashboards.
</.accordion_content>
</.accordion_item>
<.accordion_item accordion_id="item-2">
<.accordion_trigger accordion_id="item-2">Is it ejectable?</.accordion_trigger>
<.accordion_content accordion_id="item-2">
Yes. Use <code>mix phia.add</code> to copy components into your project.
</.accordion_content>
</.accordion_item>
</.accordion>
```
Uses `Phoenix.LiveView.JS` only — no external hook required.
### Dashboard Shell
```heex
<.shell>
<:topbar>
<.topbar>
<:brand>MyApp</:brand>
<.mobile_sidebar_toggle />
</.topbar>
</:topbar>
<:sidebar>
<.sidebar>
<:brand><span class="font-bold">MyApp</span></:brand>
<:nav_items>
<.sidebar_item href="/dashboard" active={@current_path == "/dashboard"}>
<.icon name="layout-dashboard" /> Dashboard
</.sidebar_item>
<.sidebar_item href="/reports">
<.icon name="bar-chart" /> Reports
</.sidebar_item>
</:nav_items>
</.sidebar>
</:sidebar>
<main class="p-6">
<%= @inner_content %>
</main>
</.shell>
```
Desktop: CSS Grid `grid-cols-[240px_1fr] h-screen`. Mobile: Flexbox drawer toggled via `Phoenix.LiveView.JS` (no Alpine.js).
### Stat Card + Metric Grid
```heex
<.metric_grid cols={4}>
<.stat_card
title="Revenue"
value="$48,290"
trend="up"
trend_value="+12.5%"
description="vs last month"
>
<:icon><.icon name="dollar-sign" size="lg" /></:icon>
</.stat_card>
<.stat_card
title="Active Users"
value="2,840"
trend="up"
trend_value="+8.2%"
description="daily active"
/>
<.stat_card
title="Churn Rate"
value="3.1%"
trend="down"
trend_value="-0.4%"
description="this month"
/>
<.stat_card
title="Avg Session"
value="4m 32s"
trend="neutral"
trend_value="0%"
description="no change"
/>
</.metric_grid>
```
### Chart Shell
```heex
<.chart_shell title="Monthly Revenue" description="Jan–Dec 2024" period="2024">
<:actions>
<.button variant="outline" size="sm">Export</.button>
</:actions>
<%!-- Drop in any chart library: VegaLite, Chart.js, D3, etc. --%>
<canvas id="revenue-chart" phx-hook="RevenueChart" />
</.chart_shell>
```
---
## ClassMerger
The `cn/1` function merges Tailwind classes with conflict resolution (last wins per group):
```elixir
import PhiaUi.ClassMerger, only: [cn: 1]
cn(["px-4 py-2", @class])
# => "px-4 py-2 mt-4" (if @class = "mt-4")
cn(["px-4", "px-8"])
# => "px-8" (conflict resolved: last wins)
cn(["text-red-500", @error && "text-destructive", @class])
# => falsy values are filtered out
```
Backed by an ETS-cached GenServer (`PhiaUi.ClassMerger.Cache`) for zero-overhead repeated calls.
---
## JS Hooks Setup
Register the four hooks in `assets/js/app.js`:
```javascript
import PhiaDialog from "./phia_hooks/dialog.js"
import PhiaDropdownMenu from "./phia_hooks/dropdown_menu.js"
import PhiaTagsInput from "./phia_hooks/tags_input.js"
import PhiaRichTextEditor from "./phia_hooks/rich_text_editor.js"
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
hooks: { PhiaDialog, PhiaDropdownMenu, PhiaTagsInput, PhiaRichTextEditor }
})
```
Hook files are copied to your project by `mix phia.install` or `mix phia.add <component>`.
| Hook | Component | Lines | Features |
|------|-----------|-------|----------|
| `PhiaDialog` | Dialog | 152 | Focus trap, Escape, scroll lock, auto-focus |
| `PhiaDropdownMenu` | Dropdown Menu | 174 | Smart positioning, click-outside, arrow key nav |
| `PhiaTagsInput` | Tags Input | 202 | Add/remove tags, deduplication, CSV sync |
| `PhiaRichTextEditor` | Rich Text Editor | 300+ | 14 toolbar commands, active state detection |
---
## Ejectable Architecture
PhiaUI is not a traditional runtime dependency. Components are source code you own:
```bash
# Eject button and card into your project
mix phia.add button card
```
This copies:
- `lib/your_app/components/button.ex`
- `lib/your_app/components/card.ex`
- `assets/js/phia_hooks/` (any required hooks)
After ejection, modify components freely — PhiaUI has no opinion on your code after the copy.
This contrasts with Tailwind UI or shadcn/ui's copy-paste model by automating the copy via Mix tasks and keeping component metadata in a registry.
---
## Use Cases
- **Financial terminals** — StatCard + MetricGrid for live P&L, position tracking, risk dashboards
- **BI dashboards** — ChartShell wrapping VegaLite/Chart.js/D3 with consistent chrome
- **KPI monitors** — Metric grids with real-time trend indicators
- **Admin panels** — Shell + Sidebar + Table + Dialog for CRUD interfaces
- **Internal tools** — Form components with Ecto changeset integration for data entry workflows
---
## Documentation
Generate docs locally:
```bash
mix docs
```
Full documentation will be published to [HexDocs](https://hexdocs.pm/phia_ui) on release.
---
## Contributing
We value **Clarity**, **Simplicity**, and **Testability**.
- All features require a specification with acceptance criteria before implementation
- TDD: write failing tests first (red → green)
- No Alpine.js, no npm deps for interactivity
- `cn/1` implemented natively — no tw_merge or similar
- All code passes `mix credo --strict` without warnings