PureAdmin supports the Pure Admin theme system with dynamic theme switching, color variants, and light/dark modes.

Available Themes

ThemePackage
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.md

Place extracted themes under priv/static/themes/ so Phoenix can serve them:

Pure Admin uses a three-file config modeled on package.json / package-lock.json:

FileRoleTracked?
pureadmin.jsondeclarations only — which themes the project usesyes (hand-edited)
pureadmin.lock.jsonresolved versions, content shas, fetch timestampsyes (tool-managed)
.pureadmin.jsonper-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 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

// 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
}
DescriptorEffectTypical range
ascent-overrideControls where glyphs sit vertically. Higher % = text moves up.85% – 120%
descent-overrideControls space below the baseline. Lower % = less descender space.10% – 40%
size-adjustScales 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-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.

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. See 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:

VariablePurpose
--pa-successSuccess / confirmation
--pa-warningWarning / caution
--pa-dangerDanger / error
--pa-infoInformational

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):

VariablePurpose
--pa-very-positiveStrong positive (e.g. ↑↑ in KPIs)
--pa-positivePositive — aliases --pa-success
--pa-neutralNo change / baseline
--pa-negativeNegative — aliases --pa-danger
--pa-very-negativeStrong 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():

VariablePurpose
--pa-text-strongHigh-contrast text (85%) — section headings, key values
--pa-text-secondarySecondary text (70%) — supporting copy, captions
--pa-text-tertiaryTertiary 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:

VariablePurpose
--pa-surface-hoverHover backdrop (4% of --pa-text-color-1 over transparent)
--pa-surface-trackTrack/rail background for gauges, progress bars (12%)
VariablePurpose
--pa-link-colorDefault link colour (aliases --pa-accent)
--pa-link-color-hoverHovered link
--pa-link-color-visitedVisited link

Chart trendline tokens (v2.7.0)

For inline SVG sparklines and trend indicators:

VariablePurpose
--pa-chart-trendline-heightDefault trendline container height (3rem)
--pa-chart-trendline-strokeSVG user-space stroke width (2.1)

Detail popover chrome (v2.7.1)

The dark-themed hover detail popover used by every KPI tile / row:

VariablePurpose
--pa-detail-bgPopover background
--pa-detail-textPopover text
--pa-detail-shadowPopover drop-shadow

Gauge size (v2.7.0)

VariablePurpose
--pa-gauge-sizeHalf-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

VariableDescription
--pa-header-bgNavbar background
--pa-sidebar-bgSidebar background
--pa-sidebar-widthSidebar width (default: 26rem)

Theme color slots

VariableDescription
--pa-color-1 through --pa-color-9Custom 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=cobalt2

The inline script in the root layout reads the query param, stores it in localStorage, and swaps the theme CSS link before paint.