# Theming
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](https://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.md
```
Place 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:
```json
{
"themesDir": "priv/static/themes",
"themes": {
"audi": {},
"dark": {},
"express": {}
}
}
```
Then resolve and download with the CLI:
```bash
# 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](https://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:
```dockerfile
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 ci
```
> **Tip:** Run the theme download step *before* `mix assets.deploy` so that `phx.digest` fingerprints 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
```scss
// 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:
```scss
// 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:
```scss
@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](https://demo.pureadmin.io/tools/font-test) to find the right values for your font.
### Complete example
Audi theme with Monda font, baseline-corrected:
```scss
// 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-face` declarations (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](https://pureadmin.io/docs/theme-customization).
## Creating Custom Themes
Use the Pure Admin CLI to scaffold and publish your own themes:
```bash
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](https://pureadmin.io/docs/creating-themes) 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:
```heex
<.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:
```heex
<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. See [`CSS-VARIABLES.md`](https://github.com/KeenMate/pure-admin/blob/main/packages/core/CSS-VARIABLES.md) in `@keenmate/pure-admin-core` for 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-core` v2.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 `:root` block 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](#theme-color-slots-1-9) above |
## Settings Panel
Add the settings panel to your layout for runtime theme/layout customization:
```heex
<.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=cobalt2
```
The inline script in the root layout reads the query param, stores it in `localStorage`, and swaps the theme CSS link before paint.