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
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.
Create a new Phoenix project without Tailwind — Pure Admin provides its own CSS framework:
mix phx.new my_app --no-tailwindIf 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.
Add keen_pure_admin to your list of dependencies in mix.exs:
def deps do
[
{:keen_pure_admin, "~> 1.0"}
]
enddef deps do
[
{:keen_pure_admin, github: "KeenMate/keen-pure-admin", tag: "v1.1.0"}
]
enddef deps do
[
{:keen_pure_admin, path: "../keen-pure-admin"}
]
endThen fetch dependencies:
mix deps.getIn 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
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
# 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()}>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/themesAdd 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.
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 }
})<script src="https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]"></script>Or install via npm:
cd assets
npm install @floating-ui/dom<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />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>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})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>Slide-out profile panel with avatar, tabs, navigation, and click-outside-to-close:
<.profile_panel name="John Doe" email="[email protected]" 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>Client-side settings panel for theme mode, layout width, sidebar options, fonts, and more -- all persisted to localStorage:
<.settings_panel />| 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 |
| 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 |
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.
| 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 |
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.
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
│ └── ...
└── ...
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 themesThe CLI tracks versions and checksums in pure-admin.json — only changed themes are re-downloaded.
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.
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-clearAll 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.
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.
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).
- Elixir ~> 1.15
- Phoenix LiveView ~> 1.0
@keenmate/pure-admin-coreCSS (v2.3.6+)
mix deps.get # Install dependencies
mix compile # Compile
mix test # Run tests
mix format # Format code
mix quality # Format check + credo + dialyzercd demo
mix setup # Install deps + build assets
mix phx.server # Visit http://localhost:4000Using 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 imageOr 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-demoFor production (elixir.demo.pureadmin.io):
SECRET_KEY_BASE=<your-secret> PHX_HOST=elixir.demo.pureadmin.io make podman-deployMIT