Keenmate's CSS foundation — descended from Yahoo's Pure CSS and extended into a
more robust, themeable layer for real apps. One small, dependency-free package gives you the
--base-* theming contract (one block of custom properties re-themes everything at once), a
modern flexbox grid (.pc-row / .pc-col, container-query responsive — replacing Pure's float
grid), a set of utility classes, and an optional app shell + JS runtime (in the
pure-css.css bundle).
It's the shared layer the whole Keenmate stack agrees on:
@keenmate/pure-admin-core builds its component library on
top of it, and every Keenmate web/Svelte component reads its colours from the same --base-*
variables.
- Reboot — native form controls take the theme's primary —
reboot.scssnow setshtml { accent-color: var(--pc-primary-color) }, so checkboxes, radios, range sliders and progress bars that nothing else styles stop using the browser's default blue and follow the theme (cobalt2's native radios showed blue next to its yellow checkboxes). It matters most on pages that use pure-css without pure-admin. Controls drawn withappearance: none— pure-admin's.pa-checkbox/.pa-radio— ignore it, andcomponent-reset.cssstill resetsaccent-colortoautoinside web components, so nothing leaks into shadow DOM.
Upgrade note: breaking. The
--pc-*role colours were renamed and the physical left / right utilities are gone. Grep your CSS and markup for--pc-(primary|secondary|success|warning|danger|info)and forml-/mr-/pl-/pr-/-left/-rightutility classes.
- Theming —
--pc-*role colours end in-color— 2.0.0-rc01 renamed only the derived names, so--pc-successsat next to--pc-success-text-color. The role colours now follow the same rule as the--base-*contract they mirror:--pc-primary/-hover/-activebecome--pc-primary-color/-color-hover/-color-active, and--pc-secondary,--pc-success,--pc-warning,--pc-danger,--pc-infobecome--pc-<role>-color. The tints (--pc-primary-bg-subtle[-hover]) and--pc-<role>-text-colorare unchanged. - Utilities — inline sides are logical-only —
.ms-*/.me-*/.ps-*/.pe-*now come in both the numeric (ms-2) and semantic (ms-sm) scales with!important, and.border-start/.border-end(plus-0) and.rounded-start/.rounded-endjoin them. They follow the writing direction, so they mirror underdir="rtl". The physical.ml-*/.mr-*/.pl-*/.pr-*,.border-left/-rightand.rounded-left/-rightare removed; rename-left→-startand-right→-end(identical in LTR)..mx-*/.px-*usemargin-inline/padding-inline. - Utilities —
.font-family-mono/.font-family-systemfollow the theme — they read--base-font-family-mono/--base-font-familyinstead of fixed stacks, so they match the framework's own code and UI text. Visible change:.font-family-monowas"Courier New"and is now the code font (SFMono-Regular, Menlo, Consolas, …by default).
pure-css is a standalone foundation you drop onto any surface — a docs site, a marketing page, a widget host, or a full application. One small, dependency-free package gives you theming, layout and utilities without buying into a component framework.
Its heart is a single --base-* theming contract: override one block of custom properties and
everything re-themes at once — the grid, the utilities, the optional app shell, and any component
that reads the same variables. Light and dark are built in via light-dark(), there's no build step
to consume it (just link the prebuilt CSS), and it pulls in no runtime dependencies.
The canonical --base-* list is owned by
@keenmate/base-css-variables (the plain-CSS
contract standalone web components ship with); pure-css mirrors it exactly, so the same theme block
works whether a page loads pure-css, the web components, or both.
@keenmate/pure-css (this package)
├─ --base-* theming contract
├─ .pc-row / .pc-col grid
├─ utility classes
└─ optional app shell + JS runtime
▲ consumed directly, as built CSS, by…
├── docs sites · portals · marketing pages · widget & component hosts
└── @keenmate/pure-admin-core — adds a full component library on top (just one consumer)
npm install @keenmate/pure-cssPrebuilt CSS (simplest):
<link rel="stylesheet" href="node_modules/@keenmate/pure-css/dist/css/pure-css.css">or cherry-pick:
<link rel="stylesheet" href="…/pure-css/dist/css/base.css"> <!-- variables only -->
<link rel="stylesheet" href="…/pure-css/dist/css/reboot.css"> <!-- 10px rem base -->
<link rel="stylesheet" href="…/pure-css/dist/css/grid.css"> <!-- + grid -->Keep
reboot.csswhen cherry-picking. Everyremin pure-css assumeshtml { font-size: 10px }(1rem= 10px;.m-4= 1rem, grid gutters 0.8rem). Without the reboot the grid and utilities render 1.6× too large on a default 16px root.
SCSS (compile $base-* overrides into the CSS):
// Configure the variables ONCE, before anything else loads them — every derived
// token (text tiers, hover/active steps, borders, control outlines…) is computed
// from these inputs. Then load the bundle: it gets the configured variables.
@use '@keenmate/pure-css/src/scss/variables' with (
$base-primary-color: #4f46e5,
$base-page-bg: #f6f8fb,
);
@use '@keenmate/pure-css/src/scss/pure-css';Compile with --load-path=node_modules. To emit only the token layer (for loading after a
prebuilt pure-css.css / base.css), @use base-css-variables instead of the bundle and
include output-base-css-variables + output-pc-css-variables in :root. The configuring
@use … with must come first: Sass modules are configured once, so a later with (or a
module that already loaded the variables) is an error rather than a silently ignored override.
| Artifact | Contents | When to link |
|---|---|---|
dist/css/pure-css.css |
everything below, in one file | the common case |
dist/css/base.css |
only :root { --base-*; --pc-*; } |
you just need the theming contract (e.g. to theme embedded web components) or a base for a theme override |
dist/css/reboot.css |
html { font-size: 10px } + box-sizing reset + neutral element styling (headings, paragraphs, links) |
always, unless you link the bundle — it's the rem contract everything else assumes |
dist/css/scrollbars.css |
thin themed scrollbars, coloured from --pc-* |
you want the browser chrome to match the theme |
dist/css/component-reset.css |
a :host reset (box-sizing + inherited typography pinned to --base-*) — the Shadow-DOM counterpart to reboot |
building a web component: adopt it into the shadow root (e.g. import '@keenmate/pure-css/component-reset?inline') so the host page can't bleed styles in; pair with base |
dist/css/grid.css |
.pc-row / .pc-col-* (percentage + fraction columns, container-query responsive) |
layout only |
dist/css/utilities.css |
spacing / flex / display / width-height, borders / rounded / shadows, role and palette colour helpers (.text-primary, .bg-success, .surface-color-3, .text-muted, …) and .font-family-* |
utilities only |
The pure-css.css bundle is base + reboot + scrollbars + grid + utilities, plus the app shell
(navbar, sidebar, layout container), which ships in no other artifact. component-reset.css is
standalone and not part of the bundle.
Every class the package emits is listed in COMPONENTS.md (machine-readable:
components.json), regenerated from the built CSS on each publish.
The shell's behaviour (nav fit/collapse, dropdowns, drag-to-resize, container
breakpoints) ships as dependency-free source JS via the ./js export — no
bundler required, drop it in with <script> tags (pure-css.js first):
<link rel="stylesheet" href="node_modules/@keenmate/pure-css/dist/css/pure-css.css">
<script src="node_modules/@keenmate/pure-css/src/js/pure-css.js"></script>
<script src="node_modules/@keenmate/pure-css/src/js/fit.js"></script>
<script src="node_modules/@keenmate/pure-css/src/js/navbar-dropdown.js"></script>
<script src="node_modules/@keenmate/pure-css/src/js/sidebar-resize.js"></script>
<script src="node_modules/@keenmate/pure-css/src/js/sidebar-flyout.js"></script>
<script src="node_modules/@keenmate/pure-css/src/js/container-breakpoint.js"></script>Each engine self-initialises on DOMContentLoaded. After injecting shell markup later (SPA route
change, server-rendered patch), re-wire it with window.pureCss.components.initAll(scope).
window.pureCss also exposes an event bus and live viewport / colorScheme /
device sources. The runtime is optional — shell CSS is authored no-JS-safe, so
the styling stands on its own and the JS only adds the interactive behaviour.
--base-* is the single source of truth for theming. Framework colors, component variables
(--pc-*) and web/svelte components all derive from it via fallback chains
(--ms-accent-color: var(--base-primary-color, #3b82f6)). Categories: primary / secondary roles,
text, background, border, input, dropdown, tooltip, contextual roles (success/danger/warning/info),
interactive and item states (hover, active, selected, focused), shadow, palette slots, typography,
border-radius, and icons. The canonical list is @keenmate/base-css-variables; pure-css's mirror is
src/scss/variables/_base.scss, and scripts/check-base-parity.mjs fails if the two drift.
Only the inputs are fixed values (surfaces, --base-text-color-1, role colours, on-fill text,
overlay, shadow, palette). Every other token defaults to a CSS formula over them — text tiers mixed
from --base-text-color-1, hover/active steps via color-mix, borders and subtle surfaces as OKLCH
lightness shifts — so a theme that sets only the inputs gets a coherent, mode-aware result.
--base-icon-* are mask-friendly SVG glyphs (Lucide defaults) for the shared UI affordances, so the
pure-css shell, pure-admin components, and the web/svelte components render the same marks and a
theme re-skins them in one place. Each is consumed via mask: var(--base-icon-x); background: currentColor, so the glyph inherits text colour — override a token with any mask-friendly url() to
swap the icon set.
| Token | Glyph | Use |
|---|---|---|
--base-icon-chevron |
stroked angle › |
expanders / nav — rotate-one-glyph disclosure (points right, rotate 90° when open) |
--base-icon-caret-down / --base-icon-caret-up |
solid triangles ▾ / ▴ |
static dropdown / <select> affordance (down) and sort-direction / upward-dropdown counterpart (up) — a caret never rotates |
--base-icon-close |
✕ |
dismiss a transient surface (dialog, panel, popover, toast) |
--base-icon-clear |
✕ |
clear a field — distinct purpose, same glyph; follows --base-icon-close, override alone to diverge |
--base-icon-remove |
✕ |
take an item out of a collection (chip / tag / row) — non-destructive; follows --base-icon-close |
--base-icon-expand / --base-icon-collapse |
+ / − |
swap-two-glyphs disclosure (tree nodes, accordions): show + when collapsed, − when open |
--base-icon-add / --base-icon-edit / --base-icon-delete |
+ / pencil / trash |
CRUD action verbs — create / modify / destroy (delete is a trash can, not an ✕, so it reads as destructive) |
--base-icon-search |
magnifying glass | search inputs, command palette — find by text |
--base-icon-filter |
funnel | refine / narrow a list by criteria (filter toggles, faceted search) — distinct from search |
--base-icon-refresh |
two curved arrows | reload / re-fetch a view or dataset |
--base-icon-check / --base-icon-indeterminate |
✓ / − |
selection pair (checkboxes, multiselect, tree nodes): check = selected, indeterminate = a tri-state parent whose children are a mix |
--base-icon-copy |
two overlapping sheets | copy-to-clipboard |
--base-icon-ellipsis |
three dots ⋯ |
"more / overflow" affordance — rotate-one-glyph for the vertical ⋮ variant (rotate 90°) |
--base-icon-save |
floppy disk | persist / commit |
--base-icon-download |
arrow into tray | download / export |
--base-icon-link / --base-icon-external-link |
chain links / box with arrow | in-app link vs. opens elsewhere (new tab, other site) |
--base-icon-settings / --base-icon-user / --base-icon-bell |
gear / person / bell | shell chrome: settings, account menu, notifications |
--base-icon-info / --base-icon-success / --base-icon-warning / --base-icon-danger |
circled i / circled ✓ / triangle ! / circled ✕ |
status marks for alerts, toasts and validation messages, one per role |
--base-icon-check-size (default 68%) sizes the check glyph inside its box, paired with
--base-checkbox-scale, which resizes the checkbox itself.
Three intentional distinctions:
- Disclosure models: chevron rotates one glyph (sidebar, multiselect), while expand/collapse swaps
two glyphs (trees, accordions) — a component never rotates a
+into a−. - ✕ vs trash:
close/clear/removeare three dismiss purposes that share the ✕ glyph (and cascade off--base-icon-close), whiledeleteis a separate destructive action drawn as a trash can.addshares the+shape withexpandbut is an independent knob (create ≠ disclosure). - Selection ≠ disclosure:
indeterminateshares the−shape withcollapsebut is its own knob — a partially-selected checkbox is not a collapsed node.
A theme is nothing but a set of --base-* values. The lightest possible theme is a stylesheet
that redeclares them, loaded after base.css:
:root {
--base-primary-color: #4f46e5;
--base-page-bg: #f6f8fb;
--base-text-color-1: #1a2233;
}Because pure-admin-core, the components and any consumer all read the same variables, that one block
re-themes all of them at once. This is the same model as
@keenmate/pure-admin-themes, so the same CLI and
publishing infrastructure applies.
The defaults are light-dark() pairs keyed off the CSS color-scheme. base.css sets
color-scheme: light, so with pure-css alone dark mode is one declaration:
:root { color-scheme: dark; } /* always dark */
:root { color-scheme: light dark; } /* follow the OS setting */Themes (and pure-admin) switch light/dark and colour variants by toggling a class — .pc-mode-light /
.pc-mode-dark, and pure-admin's .pa-color-*. Apply these to the :root element (<html>), not
<body>.
The mode/variant blocks override input tokens (--pc-* / --base-*). Many themed tokens are
derived from those inputs and emitted once at :root — e.g. core emits
--pa-btn-info-bg: var(--pc-info-color). CSS resolves a custom property's var() at the element that
declares it, so a derived token declared on :root bakes in :root's input value. If the mode
class sits on a descendant (<body>), the override comes too late and the derived token stays
frozen at the default-mode value — the classic symptom is a role button or surface that doesn't
change colour when you switch modes. Putting the class on :root (the same element that declares
the tokens) makes the overrides win and the derived tokens re-resolve.
pure-css re-emits its own base text-tier tokens at :root, .pc-mode-light, .pc-mode-dark to tolerate
either placement, but that does not extend to the pure-admin component layer, hence the :root rule.
To avoid a colour "flash" on switch, disable transitions for one frame during the swap (add a
transition: none !important class to :root, change the mode/variant class, force a reflow, then
remove it).
make install # npm install (sass; vite + playwright for the demo and tests)
make build # src/scss -> dist/css (all 7 artifacts)
make sizes # show artifact sizes
npm run dev # demo site on http://localhost:12500
npm test # Playwright e2e suite (first run: npm run test:e2e:install)
npm run catalog # regenerate COMPONENTS.md / components.json
node scripts/check-base-parity.mjs # --base-* names match @keenmate/base-css-variablesdist/ is committed so consumers can vendor the built CSS without a Sass toolchain.
The SCSS is the foundation extracted from pure-admin-core's src/scss — the variables/ modules,
_base-css-variables.scss, utilities.scss, _fonts.scss, and the native grid (_pa-grid.scss,
formerly core's core-components/_grid.scss). pure-admin-core now consumes this package as its
single source for the foundation (thin @forward / @use shims), so the two no longer drift —
core's compiled --base-* values and grid output match pure-css exactly.
One intentional difference: utilities.scss here @uses _fonts.scss so the generic
.font-family-* classes ship with the other utilities, whereas core keeps _fonts.scss standalone.
_rtl-helpers.scss and the component layer stay in core.
MIT © Keenmate s.r.o. The grid is derived from Pure (Yahoo!, BSD).