PureAdmin supports the Pure Admin theme system with dynamic theme switching, color variants, and light/dark modes.
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 |
Browse and preview all themes at pureadmin.io.
Installing Themes
Theme zips are self-contained — the compiled CSS in dist/ references fonts via relative paths (../assets/fonts/...), so extracting a zip preserves correct asset resolution without any path adjustments.
Each theme zip contains:
audi/
├── theme.json # metadata: colors, variants, modes, fonts, checksums
├── dist/
│ └── audi.css # compiled CSS (ready to use)
├── scss/
│ └── audi.scss # SCSS source (for customization, see below)
├── assets/
│ └── fonts/
│ ├── *.woff2 # bundled font files
│ └── ...
└── README.mdPlace extracted themes under priv/static/themes/ so Phoenix can serve them:
Option A: Pure Admin CLI (recommended)
Pure Admin uses a three-file config modeled on package.json / package-lock.json:
| File | Role | Tracked? |
|---|---|---|
pureadmin.json | declarations only — which themes the project uses | yes (hand-edited) |
pureadmin.lock.json | resolved versions, content shas, fetch timestamps | yes (tool-managed) |
.pureadmin.json | per-developer overrides (local paths, dev API keys) | no (gitignored) |
Create pureadmin.json at your project root with the themes you ship:
{
"themesDir": "priv/static/themes",
"themes": {
"audi": {},
"dark": {},
"express": {}
}
}Then resolve and download with the CLI:
# Local dev: install + write/refresh the lockfile
npx @keenmate/pureadmin themes install
# Bump every theme to the latest registry version (writes the lock)
npx @keenmate/pureadmin themes update
# CI / Docker: strict reproduce from the lockfile, fail on drift, never write
npx @keenmate/pureadmin themes ci
themes install is the everyday "make this project work" verb — fresh clones run it once. themes ci is the strict CI verb that reproduces the lockfile exactly. Add priv/static/themes/ and .pureadmin.json to .gitignore.
The CLI auto-detects your @keenmate/pure-admin-core version (probes package.json and assets/package.json) and resolves theme versions compatible with it.
Option B: Manual download
Download theme zips from pureadmin.io and extract them into priv/static/themes/. Each zip extracts to <id>/css/<id>.css plus assets and a theme.json manifest.
Option C: Download in CI/CD (Dockerfile)
Run themes ci during your Docker build. Copy the two config files first so the CLI knows what to fetch, then run the install:
COPY pureadmin.json pureadmin.lock.json ./
RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/* \
&& npx @keenmate/pureadmin themes ciTip: Run the theme download step before
mix assets.deployso thatphx.digestfingerprints the theme files along with the rest of your static assets.
See the Dockerfile in the repo root for a complete working example.
Customizing Themes via SCSS
If you install themes via npm (@keenmate/pure-admin-theme-*), you can import their SCSS source into your project stylesheet and override variables before the import. All theme variables use !default, so your values take precedence:
Basic variable override
// assets/css/app.scss
// Override variables before importing the theme
$base-accent-color: #0066cc;
$card-border-radius: 8px;
// Import the theme — your overrides win
@import '@keenmate/pure-admin-theme-audi/src/scss/audi';Custom font
Every theme bundles its own font (e.g., Audi bundles Fira Sans Condensed). To use a different font, override $base-font-family and declare your @font-face before the theme import:
// assets/css/app.scss
// 1. Set your font family (overrides the theme's bundled font)
$base-font-family: 'Monda', Arial, sans-serif;
// 2. Declare @font-face with your font files
@font-face {
font-family: 'Monda';
font-weight: 400;
font-display: swap;
src: url('./fonts/monda-400.woff2') format('woff2');
}
@font-face {
font-family: 'Monda';
font-weight: 700;
font-display: swap;
src: url('./fonts/monda-700.woff2') format('woff2');
}
// 3. Import the theme
@import '@keenmate/pure-admin-theme-audi/src/scss/audi';Tip: Bundle font files locally in your project rather than loading from a CDN. The theme's bundled font is local and renders first — a remote font arriving later causes a visible flash (FOUT).
Font baseline correction
Different fonts have different vertical metrics. When you swap fonts, text may appear higher or lower within buttons, card headers, and other aligned components. Use ascent-override and descent-override in @font-face to correct this:
@font-face {
font-family: 'Monda';
font-weight: 400;
font-display: swap;
src: url('./fonts/monda-400.woff2') format('woff2');
ascent-override: 110%; // push glyphs up within the line box
descent-override: 20%; // reduce space below the baseline
}| Descriptor | Effect | Typical range |
|---|---|---|
ascent-override | Controls where glyphs sit vertically. Higher % = text moves up. | 85% – 120% |
descent-override | Controls space below the baseline. Lower % = less descender space. | 10% – 40% |
size-adjust | Scales the font without changing font-size. Affects width and height. | 90% – 115% |
Use the Font Tuning Tool to find the right values for your font.
Complete example
Audi theme with Monda font, baseline-corrected:
// assets/css/app.scss
$base-font-family: 'Monda', Arial, sans-serif;
@font-face {
font-family: 'Monda';
font-weight: 400;
font-display: swap;
src: url('./fonts/monda-400.woff2') format('woff2');
ascent-override: 110%;
descent-override: 20%;
}
@font-face {
font-family: 'Monda';
font-weight: 700;
font-display: swap;
src: url('./fonts/monda-700.woff2') format('woff2');
ascent-override: 110%;
descent-override: 20%;
}
// Import theme — uses Monda everywhere instead of Fira Sans Condensed
@import '@keenmate/pure-admin-theme-audi/src/scss/audi';The theme's original
@font-facedeclarations (e.g., Fira Sans Condensed) remain in the compiled CSS but are never used since nothing references that font family name. This adds a few KB of unused CSS but has no runtime impact.
For more details see the Theme Customization guide on pureadmin.io.
Creating Custom Themes
Use the Pure Admin CLI to scaffold and publish your own themes:
npm install -g @keenmate/pureadmin
# Scaffold a new theme project
pureadmin init my-theme "My Theme"
# Edit src/scss/my-theme.scss, then build and preview
pureadmin build
# Package with integrity checksums
pureadmin pack
# Publish to pureadmin.io (requires API key)
pureadmin publish --api-key YOUR_KEY
The CLI handles SCSS compilation, font bundling, SHA-256 checksums, and ZIP packaging. Published themes are immediately available via the API and CLI for other projects.
See the Creating Themes guide on pureadmin.io for full documentation.
Theme Color Slots (1-9)
Every theme defines 9 custom color slots. These are used by components via the theme_color attribute:
<.alert theme_color="3">Custom branded alert</.alert>
<.button theme_color="5">Custom button</.button>
<.callout theme_color="1">Custom callout</.callout>Components supporting theme_color: alert/1, button/1, callout/1, toast/1, card/1, table_card/1, input/1, select/1, textarea/1.
Light / Dark Mode
Mode is managed client-side via the settings panel. The fouc_prevention_script applies the stored mode before paint to prevent flashing:
<body>
<.fouc_prevention_script default_mode="auto" />
{@inner_content}
</body>default_mode controls the first-visit mode (before any user selection is stored). Accepts "light", "dark", or "auto" (follows OS prefers-color-scheme). Defaults to "light".
CSS classes applied to <body>: pa-mode-light or pa-mode-dark (auto resolves to one of these at runtime).
Theme CSS Variables
Pure Admin exposes ~195 CSS custom properties split into two layers:
--base-*(~71 vars) — web-component-style design tokens. Stable, semantic, override-friendly. SeeCSS-VARIABLES.mdin@keenmate/pure-admin-corefor the full reference.--pa-*(~124 vars) — framework component tokens. Derived from the--base-*layer; most of them aren't intended to be overridden directly, but are useful for ad-hoc styling.
As of
@keenmate/pure-admin-corev2.8.0, the unthemed bundle (dist/css/main.css) emits a complete neutral default for every--pa-*token at:root. This means the framework renders with reasonable defaults before a theme stylesheet loads, eliminating the FOUC window where sparklines / sentiment indicators rendered near-black. Themes still emit their own:rootblock on top.
Canonical role tokens (v2.8.0)
The four "what's the user trying to communicate" tokens. Use these instead of hard-coding red/green/yellow/blue:
| Variable | Purpose |
|---|---|
--pa-success | Success / confirmation |
--pa-warning | Warning / caution |
--pa-danger | Danger / error |
--pa-info | Informational |
Each role also has a *-bg / *-bg-hover / *-bg-light / *-bg-subtle / *-border / *-text / *-text-light family for component-level styling (e.g. --pa-success-bg-light for alert backgrounds).
5-step sentiment scale (v2.6.0, refined in v2.8.0)
For data visualization where "positive vs. negative" is the axis (KPI deltas, trend arrows, comparison gauges):
| Variable | Purpose |
|---|---|
--pa-very-positive | Strong positive (e.g. ↑↑ in KPIs) |
--pa-positive | Positive — aliases --pa-success |
--pa-neutral | No change / baseline |
--pa-negative | Negative — aliases --pa-danger |
--pa-very-negative | Strong negative (e.g. ↓↓) |
Used by Stat's 5-step change_direction attr (very_positive / positive / neutral / negative / very_negative) and across the KPI component family.
Text contrast tiers (v2.8.0)
Three semantic text colours derived from --pa-text-color-1 via color-mix():
| Variable | Purpose |
|---|---|
--pa-text-strong | High-contrast text (85%) — section headings, key values |
--pa-text-secondary | Secondary text (70%) — supporting copy, captions |
--pa-text-tertiary | Tertiary text (55%) — labels, hints, timestamps |
These work on both light and dark modes without needing per-mode overrides — the base colour flips, the mixing percentage stays the same.
Surface tints (v2.8.0)
For hover backdrops and "track" backgrounds in progress / gauge components:
| Variable | Purpose |
|---|---|
--pa-surface-hover | Hover backdrop (4% of --pa-text-color-1 over transparent) |
--pa-surface-track | Track/rail background for gauges, progress bars (12%) |
Link tokens (v2.7.0)
| Variable | Purpose |
|---|---|
--pa-link-color | Default link colour (aliases --pa-accent) |
--pa-link-color-hover | Hovered link |
--pa-link-color-visited | Visited link |
Chart trendline tokens (v2.7.0)
For inline SVG sparklines and trend indicators:
| Variable | Purpose |
|---|---|
--pa-chart-trendline-height | Default trendline container height (3rem) |
--pa-chart-trendline-stroke | SVG user-space stroke width (2.1) |
Detail popover chrome (v2.7.1)
The dark-themed hover detail popover used by every KPI tile / row:
| Variable | Purpose |
|---|---|
--pa-detail-bg | Popover background |
--pa-detail-text | Popover text |
--pa-detail-shadow | Popover drop-shadow |
Gauge size (v2.7.0)
| Variable | Purpose |
|---|---|
--pa-gauge-size | Half-donut gauge diameter (default 12rem). Override per-instance via the :size attr on gauge/1. |
KPI namespaced tokens (v2.7.1)
Per-component cascade variables under --pa-kpi-* — e.g. --pa-kpi-bar-color (sentiment-tinted bars in comparison gauges), --pa-kpi-edit-cell-min (auto-fit cell minimum for editorial grids), --pa-kpi-gauge-cell-min, --pa-kpi-bento-row-height. Most are set via component attrs (cell_min_width, row_height) rather than via global theme overrides.
Layout
| Variable | Description |
|---|---|
--pa-header-bg | Navbar background |
--pa-sidebar-bg | Sidebar background |
--pa-sidebar-width | Sidebar width (default: 26rem) |
Theme color slots
| Variable | Description |
|---|---|
--pa-color-1 through --pa-color-9 | Custom branded colour slots — see Theme Color Slots above |
Settings Panel
Add the settings panel to your layout for runtime theme/layout customization:
<.settings_panel default_theme="audi" />The panel fetches available themes from /api/themes/manifests and populates the selector dynamically. All settings persist to localStorage:
- Theme selection
- Color variant (per theme)
- Light/dark mode
- Layout width (fluid, sm, md, lg, xl, 2xl)
- Sidebar behavior (hide, icon-collapse, resizable, sticky)
- Font size and family
- Compact mode, RTL mode
Dynamic Theme Switching
Use ?theme=name query parameter to switch themes:
https://your-app.com/?theme=dark
https://your-app.com/?theme=cobalt2The inline script in the root layout reads the query param, stores it in localStorage, and swaps the theme CSS link before paint.