Phoenix LiveView component library wrapping the Pure Admin CSS framework into function components and LiveComponents.
Drop-in replacement for Phoenix CoreComponents -- provides button/1, badge/1, card/1, modal/1, table/1, input/1, and 35+ more components with full BEM class support.
Main site: pureadmin.io — themes, documentation, and component showcase
Live demo: elixir.demo.pureadmin.io
What's new in v1.1.0
field={@form[:x]}on form components —input/1,textarea/1,select/1,checkbox/1,radio/1, andform_group/1now accept a PhoenixPhoenix.HTML.FormFieldand derivename,id,value(orchecked), and error state automatically.input/textarea/selectalso auto-render the errorform_helpbelow themselves — no per-field boilerplate.PureAdmin.Components.Form.translate_error/1— ships a default%{key}-interpolating formatter; override withconfig :keen_pure_admin, :error_formatter, {MyAppWeb.CoreComponents, :translate_error}for Gettext-aware apps.PureAdmin.DateTime— date/time/relative formatting helper withformat/2(short/long date, short/long date-time, time, relative, or raw strftime) andrelative/2(now,5 minutes ago,in 2 hours, etc.). Month names, weekday names, and relative phrases all flow throughPureAdmin.Translations.t/2with 47 new keys underpureAdmin.datetime.*.- Flash —
replace: true+clear_flash/2—push_flash(..., replace: true)wipes any prior alerts in the container so status messages don't stack;clear_flash/2empties it without pushing.
See the full CHANGELOG for details.
Prerequisites
Create a new Phoenix project without Tailwind — Pure Admin provides its own CSS framework:
mix phx.new my_app --no-tailwind
If you have an existing project that uses Tailwind, remove the Tailwind dependency and its configuration before adding Pure Admin, as the two CSS frameworks will conflict.
Installation
Add keen_pure_admin to your list of dependencies in mix.exs:
From Hex (recommended)
def deps do
[
{:keen_pure_admin, "~> 1.0"}
]
endFrom GitHub
def deps do
[
{:keen_pure_admin, github: "KeenMate/keen-pure-admin", tag: "v1.1.0"}
]
endLocal path (for development)
def deps do
[
{:keen_pure_admin, path: "../keen-pure-admin"}
]
endThen fetch dependencies:
mix deps.get
Setup
1. Replace CoreComponents import
In your MyAppWeb module (e.g. lib/my_app_web.ex), find the html_helpers function and replace:
- import MyAppWeb.CoreComponents
+ use PureAdmin.ComponentsThis replaces button/1, input/1, simple_form/1, modal/1, table/1, list/1, label/1, flash/1, and flash_group/1. A few CoreComponents functions are not replaced:
header/1— use@page_titlein<.navbar_title>(the layout renders it, each LiveView sets it)icon/1— use Font Awesome directly:<i class="fa-solid fa-user"></i>translate_error/1—PureAdmin.Components.Form.translate_error/1ships a plain%{key}-interpolating default. For Gettext, setconfig :keen_pure_admin, :error_formatter, {MyAppWeb.CoreComponents, :translate_error}(MFA tuple or 1-arity function) and errors flow through your existing pipeline.show/1,hide/1— usePhoenix.LiveView.JS.show/1andJS.hide/1directly
2. Replace the generated layouts
Phoenix generates a layouts.ex with inline app/1 and flash_group/1 functions that conflict with PureAdmin. Replace it:
# lib/my_app_web/components/layouts.ex
defmodule MyAppWeb.Layouts do
use MyAppWeb, :html
embed_templates "layouts/*"
endThen create lib/my_app_web/components/layouts/app.html.heex with a PureAdmin layout:
<.layout>
<.navbar>
<:start>
<.navbar_burger />
<.navbar_brand />
</:start>
<:center>
<.navbar_title>
<h2>{assigns[:page_title] || "Home"}</h2>
</.navbar_title>
</:center>
</.navbar>
<.layout_inner>
<.sidebar>
<.sidebar_item label="Home" icon="fa-solid fa-house" href="/" />
</.sidebar>
<.layout_content>
<.main>
<.flash_group flash={@flash} />
{@inner_content}
</.main>
<.footer />
</.layout_content>
</.layout_inner>
</.layout>Add the app layout to your router's browser pipeline (Phoenix 1.8 doesn't set this by default — the generated code used an inline app/1 function instead):
# lib/my_app_web/router.ex
pipeline :browser do
# ... existing plugs ...
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :put_layout, html: {MyAppWeb.Layouts, :app} # <-- add this line
# ...
endAlso clean up these generated files that use Tailwind classes or CoreComponents functions:
- Delete
lib/my_app_web/components/core_components.ex— no longer needed - Delete
priv/static/assets/default.css— Phoenix default styles that conflict with Pure Admin - Replace
lib/my_app_web/controllers/page_html/home.html.heex— the generated page uses Tailwind classes andLayouts.flash_groupwhich no longer exists
3. Configure your app (optional)
# config/config.exs
config :keen_pure_admin,
app_name: "My App",
app_version: "1.0.0",
copyright: "© 2026 My Company",
font_class: "pa-font-responsive"The navbar_brand and footer components read from this config automatically. Add the font class to <html> in your root layout:
<html lang="en" {PureAdmin.Config.root_html_attrs()}>4. Install a theme
Theme CSS files include the core framework — you only need a theme. Download one using the PureAdmin CLI:
npx @keenmate/pureadmin themes add audi --dir priv/static/themes
Add themes to your static paths so Phoenix serves the files:
# lib/my_app_web.ex
def static_paths, do: ~w(assets fonts images themes favicon.ico robots.txt)Then link the theme in your root.html.heex:
<link rel="stylesheet" href="/themes/audi/css/audi.css" />Browse all available themes at pureadmin.io.
Remove the Phoenix-generated
default.csslink — it contains default Phoenix styles that conflict with Pure Admin.
5. Register JS hooks
Add PureAdminHooks to your LiveSocket in assets/js/app.js:
import { PureAdminHooks } from "keen_pure_admin"
// Merge with any existing hooks (e.g. colocatedHooks)
const liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...colocatedHooks, ...PureAdminHooks }
})6. Add Floating UI (required for tooltips, popovers, split buttons)
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/core@1.6.9"></script>
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.13"></script>Or install via npm:
cd assets
npm install @floating-ui/dom
7. Add Font Awesome (icons)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />8. Add FOUC prevention (optional)
In your root layout, add the script before {@inner_content} to prevent flash of unstyled content when using the settings panel or sidebar submenus:
<body>
<.fouc_prevention_script />
{@inner_content}
</body>9. Add global toast service (optional)
Add a toast container to your app layout for app-wide toast notifications:
<.toast_container id="toasts" position="top-end" is_hook />Then push toasts from any LiveView:
alias PureAdmin.Components.Toast, as: PureToast
socket |> PureToast.push_toast("success", "Saved!", "Changes saved successfully.")For cross-page delivery (e.g., background tasks that complete after navigation), broadcast via PubSub and handle in an on_mount hook:
# In your on_mount hook
Phoenix.PubSub.subscribe(MyApp.PubSub, "toasts")
attach_hook(socket, :global_toasts, :handle_info, fn
{:push_toast, variant, title, message, opts}, socket ->
{:halt, PureToast.push_toast(socket, variant, title, message, opts)}
_other, socket ->
{:cont, socket}
end)
# From a background task
Phoenix.PubSub.broadcast(MyApp.PubSub, "toasts",
{:push_toast, "success", "Done!", "Task completed.", duration: 0})Components
Layout
Full page structure matching the Pure Admin three-section navbar + sidebar + content + footer pattern:
<.layout>
<.navbar>
<:start>
<.navbar_burger />
<.navbar_brand><.heading level="1">My App</.heading></.navbar_brand>
<.navbar_nav>
<.navbar_nav_item href="/">Dashboard</.navbar_nav_item>
<.navbar_nav_item href="/reports" has_dropdown>
Reports
<:dropdown>
<.navbar_dropdown>
<.navbar_nav_item href="/reports/sales">Sales</.navbar_nav_item>
<.navbar_nav_item href="/reports/users">Users</.navbar_nav_item>
</.navbar_dropdown>
</:dropdown>
</.navbar_nav_item>
</.navbar_nav>
</:start>
<:center>
<.navbar_title><.heading level="2">Dashboard</.heading></.navbar_title>
</:center>
<:end_>
<.notifications count={3}>
<.notification_item variant="primary" icon="fa-solid fa-bell" is_unread>
<:title>New message</:title>
<:text>You have a new message</:text>
<:time>2 min ago</:time>
</.notification_item>
</.notifications>
<.navbar_profile_btn name="John Doe" phx-click={toggle_profile_panel()} />
</:end_>
</.navbar>
<.layout_inner>
<.sidebar>
<.sidebar_item label="Dashboard" icon="fa-solid fa-gauge" href="/" is_active />
<.sidebar_submenu id="settings" label="Settings" icon="fa-solid fa-gear" is_open={String.starts_with?(@current_path, "/settings")}>
<.sidebar_item label="General" href="/settings" />
<.sidebar_item label="Security" href="/settings/security" />
</.sidebar_submenu>
</.sidebar>
<.layout_content>
<.main>
<.flash_group flash={@flash} />
{@inner_content}
</.main>
<.footer>
<:start>© 2026 My App</:start>
</.footer>
</.layout_content>
</.layout_inner>
</.layout>Profile Panel
Slide-out profile panel with avatar, tabs, navigation, and click-outside-to-close:
<.profile_panel name="John Doe" email="john@example.com" role="Admin">
<:tabs>
<div class="pa-tabs pa-tabs--full">
<button class="pa-tabs__item pa-tabs__item--active" data-profile-tab="profile">
<i class="fa-solid fa-user"></i>
<span class="pa-profile-panel__tab-text">Profile</span>
</button>
<button class="pa-tabs__item" data-profile-tab="favorites">
<i class="fa-solid fa-star"></i>
<span class="pa-profile-panel__tab-text">Favorites</span>
</button>
</div>
</:tabs>
<div class="pa-tabs__panel pa-tabs__panel--active" data-profile-panel="profile">
<nav class="pa-profile-panel__nav">
<ul>
<.profile_nav_item href="/profile" icon="fa-solid fa-user">Settings</.profile_nav_item>
<.profile_nav_item href="/logout" icon="fa-solid fa-right-from-bracket">Sign Out</.profile_nav_item>
</ul>
</nav>
</div>
<:footer_>
<button class="pa-btn pa-btn--danger pa-btn--block">Sign Out</button>
</:footer_>
</.profile_panel>Settings Panel
Client-side settings panel for theme mode, layout width, sidebar options, fonts, and more -- all persisted to localStorage:
<.settings_panel />UI Components
| Component | Description |
|---|---|
button/1, split_button/1, button_group/1 | Buttons with variants, sizes, loading, split dropdown, responsive groups |
badge/1, label/1, composite_badge/1, badge_group/1 | Badges, labels, composite badges with expand/collapse |
alert/1 | Dismissible alerts |
callout/1 | Callout/info boxes |
card/1 | Cards with header (title/subtitle/description), body, footer, tabs |
modal/1 | Modal dialogs |
popconfirm/1 | Popconfirm dialogs anchored to trigger buttons |
table/1, table_card/1, table_container/1 | Data tables with sorting, card wrappers, responsive grid |
comparison_table/1, comparison_row/1, comparison_value/1 | Two/three-column data comparison with change/conflict highlighting |
tabs/1 | Tab navigation with panels |
input/1, textarea/1, select/1, checkbox/1, radio/1, form_group/1, input_wrapper/1 | Form inputs with labels, errors, clear button. Accept field={@form[:x]} for one-line Phoenix form binding (derives name/id/value/checked + auto-renders errors) |
filter_card/1 | Expandable filter card with advanced filters |
grid/1, column/1 | Flexbox grid with percentage/fraction columns |
section/1 | Content section with optional title_text heading |
stat/1 | Stat cards (hero, square) |
timeline/1 | Timeline displays |
loader/1, loader_center/1, loader_overlay/1 | Loading spinners |
basic_list/1, ordered_list/1, definition_list/1 | HTML lists with spacing, icons, borders |
checkbox_list/1, checkbox_list_item/1, checkbox_box/1 | Checkbox lists with variants, layouts, actions |
list/1, list_item/1 | Complex lists with avatar, title, subtitle, meta |
code/1, code_block/1 | Inline code and code blocks |
tooltip/1, popover/1 | Tooltips and popovers with Floating UI positioning |
toast/1, toast_container/1, push_toast/5 | Toast notifications with client-side rendering via JS hook |
flash/1, flash_group/1, flash_container/1, push_flash/5, clear_flash/2 | Flash messages — standard @flash compat + independent containers with markdown, action buttons, and replace: true to wipe prior alerts in the container |
pager/1, load_more/1 | Pagination with page input, first/last buttons |
JS Hooks
| Hook | Description |
|---|---|
PureAdminSettings | Settings panel localStorage management |
PureAdminProfilePanel | Profile panel tabs, favorites, click-outside |
PureAdminTooltip | Tooltip positioning |
PureAdminPopover | Popover positioning |
PureAdminToast | Toast auto-dismiss |
PureAdminFlash | Independent inline flash containers with markdown and action buttons |
PureAdminCommandPalette | Command palette: multi-step commands (/), scoped search (:), keyboard nav |
PureAdminDetailPanel | Detail panel toggle |
PureAdminSidebarResize | Drag-to-resize sidebar |
PureAdminCharCounter | Character counter with translatable messages |
PureAdminCheckbox | Tri-state checkbox indeterminate sync |
PureAdminSplitButton | Split button dropdown via Floating UI |
PureAdminSidebarSubmenu | Sidebar submenu localStorage persistence |
PureAdminInfiniteScroll | IntersectionObserver-based infinite scroll |
CSS Framework
All classes follow the BEM pattern: pa-{block}, pa-{block}--{modifier}, pa-{block}__{element}.
Browse the live component showcase and theme previews at pureadmin.io.
Available Themes
| Theme | Package |
|---|---|
| Default | @keenmate/pure-admin-core |
| Audi | @keenmate/pure-admin-theme-audi |
| Corporate | @keenmate/pure-admin-theme-corporate |
| Dark | @keenmate/pure-admin-theme-dark |
| Express | @keenmate/pure-admin-theme-express |
| Minimal | @keenmate/pure-admin-theme-minimal |
Installing Themes
Theme zips are self-contained — compiled CSS in dist/ references fonts via relative paths (../assets/fonts/...), so extracting preserves correct asset resolution with no path adjustments needed. Each theme includes compiled CSS, SCSS source (for customization), bundled fonts, and a theme.json manifest.
Option A: Manual download
Download theme zips from pureadmin.io and extract them into priv/static/themes/:
priv/static/themes/
├── audi/
│ ├── theme.json
│ ├── dist/audi.css
│ ├── scss/audi.scss
│ └── assets/fonts/*.woff2
├── dark/
│ ├── dist/dark.css
│ └── ...
└── ...Option B: Pure Admin CLI
Install the @keenmate/pureadmin CLI and manage themes in your project:
npm install -g @keenmate/pureadmin
pureadmin themes add audi dark express # download and extract
pureadmin update # re-download only changed themes
The CLI tracks versions and checksums in pure-admin.json — only changed themes are re-downloaded.
Option C: Download during CI/CD build
Fetch themes automatically in your Dockerfile using the bundle API:
ARG THEMES_URL=https://pureadmin.io/api/bundle?themes=audi,dark,express,corporate,minimal
RUN apt-get update && apt-get install -y --no-install-recommends curl unzip && rm -rf /var/lib/apt/lists/* \
&& mkdir -p priv/static/themes \
&& curl -fsSL -o /tmp/themes.zip "${THEMES_URL}" \
&& unzip -o /tmp/themes.zip -d priv/static/themes \
&& rm -f /tmp/themes.zipPass a comma-separated list of theme names to the themes query parameter. The API returns a single zip with all requested themes. See the Dockerfile in the repo root for a complete example.
Theme cache invalidation
The demo app's ThemePlug caches downloaded themes to disk. Each theme's theme.json contains a checksums.content_sha field — a SHA-256 hash of the package contents. On access, the plug validates the cache in the background by sending a conditional request (If-None-Match: <content_sha>) to pureadmin.io. If the server returns 200 (theme updated), it re-downloads without blocking the current request. Freshness checks are throttled to once per 10 minutes per theme.
To force-clear the cache:
make themes-clear
Translations (i18n)
All user-facing strings in components are translatable via a runtime callback. Without configuration, English defaults are used.
# config/config.exs
config :keen_pure_admin,
translate: &MyApp.Translations.translate/2The callback receives a flat key and a params map:
defmodule MyApp.Translations do
def translate(key, params) do
# Load from DB, Gettext, ETS — whatever fits your app
translation = MyApp.Repo.get_translation(key, current_locale())
PureAdmin.Translations.interpolate(translation, params)
end
endKeys follow the pureAdmin.* convention (e.g., pureAdmin.buttons.cancel, pureAdmin.pagination.nextPage, pureAdmin.commandPalette.searching). See PureAdmin.Translations.defaults() for the full list.
Page Context
Server-rendered JSON in a hidden input, available to JS synchronously — no API fetch needed. CSP-safe.
<%!-- In your root layout --%>
<.page_context />Register providers via config:
config :keen_pure_admin,
page_context_providers: [
&MyApp.PageContext.theme_manifests/1,
&MyApp.PageContext.user_context/1
]Each provider receives assigns and returns a map merged into the context. The settings panel reads themeManifests from the context automatically (falls back to API if missing). See PureAdmin.PageContext for details.
Logging
All JS hooks use a categorized logger — silent by default, zero overhead in production.
// Enable in browser console
PureAdmin.logging.enableLogging()
// Or per-category
PureAdmin.logging.setCategoryLevel('PA:SETTINGS', 'debug')
// List categories
PureAdmin.logging.getCategories()
// => ["PA:SETTINGS", "PA:CMD_PALETTE", ...]Also available via window.components['keen-pure-admin'].logging (KeenMate convention).
Requirements
- Elixir ~> 1.15
- Phoenix LiveView ~> 1.0
@keenmate/pure-admin-coreCSS (v2.3.6+)
Development
mix deps.get # Install dependencies
mix compile # Compile
mix test # Run tests
mix format # Format code
mix quality # Format check + credo + dialyzer
Demo App
cd demo
mix setup # Install deps + build assets
mix phx.server # Visit http://localhost:4000
Running the Demo with Podman
Using Make (recommended):
make podman-build # Build the image
make podman-run # Run the container (port 4000)
make podman-deploy # Build + run in one step
make podman-push # Push to registry.km8.es
make podman-logs # Tail container logs
make podman-stop # Stop the container
make podman-clean # Remove container and image
Or manually:
podman build -t keen-pure-admin-demo .
podman run -p 4000:4000 \
-e SECRET_KEY_BASE=$(mix phx.gen.secret) \
-e PHX_HOST=localhost \
keen-pure-admin-demo
For production (elixir.demo.pureadmin.io):
SECRET_KEY_BASE=<your-secret> PHX_HOST=elixir.demo.pureadmin.io make podman-deploy
License
MIT