The shared base layer of CSS custom properties (--base-*) used by all
KeenMate web components — web-multiselect,
web-daterangepicker,
web-grid, web-treeview, and more.
Each component ships its own prefixed variables (--ms-*, --drp-*, --wg-*, …)
that cascade from these --base-* values. Define the base layer once and every
component picks up a consistent, coordinated theme. Override a single --base-*
variable and the change propagates everywhere.
- Checkboxes —
--base-checkbox-scale+ a canonical render method. A new token scales the checkbox box (multiplying its width/height), so one override resizes every KeenMate checkbox in lockstep. It pairs with a documented render method: size the box withcalc(<base-size> * var(--base-checkbox-scale))and the glyph with--base-icon-check-size, and apply no CSS transform. Atransform: scale()promotes the box to its own compositing layer, which pixel-snaps the fractionally-positioned mask glyph and shifts the tick/dash off-centre — inconsistently from row to row. Scaling the box directly keeps the mark crisp at every scale.
- Icons —
--base-icon-check-sizescales the selection glyph — a new token sets the mask-size of the check / indeterminate mark inside its box (checkbox / tree-node), defaulting to68%so it sits with breathing room. Override it once to re-scale the selection mark everywhere in lockstep — handy when swapping--base-icon-checkfor a glyph that fills its viewBox edge-to-edge.@keenmate/pure-cssmirrors it as$base-icon-check-size.
npm install @keenmate/base-css-variablesImport the stylesheet once, as early as possible (before the component styles):
import '@keenmate/base-css-variables/base-variables.css';or in CSS / HTML:
@import '@keenmate/base-css-variables/base-variables.css';<link rel="stylesheet" href="node_modules/@keenmate/base-css-variables/base-variables.css">The base layer is a set of semantic design tokens, not raw colors. Several tokens may resolve to the same default value yet exist as separate variables so each role can be themed independently.
--base-main-bg and --base-input-bg both default to white (light) / near-black
(dark), but they mean different things:
| Token | Meaning | Example component consumers |
|---|---|---|
--base-main-bg |
The global / primary surface — the canvas an app shell, grid, dropzone or panel paints itself on | --wg-surface-1 (grid surface), --drp-primary-bg (calendar panel), --ms-hint-bg, --ms-actions-bg |
--base-input-bg |
The background of form fields specifically | --ms-input-bg, --drp-input-bg, --wg-input-bg |
Because they are separate variables, you can, for example, keep a grid or dropzone on a plain page background while giving input fields a subtly tinted fill:
:root {
--base-main-bg: #ffffff; /* page / grid / dropzone canvas */
--base-input-bg: #f7f9fc; /* inputs stand out slightly */
}If you only set --base-main-bg, inputs keep their own default — they don't
inherit from it. Set both when you want them to match.
Background surfaces climb an elevation ladder — each step a little more prominent than the last — with a separate interaction-state axis on top:
--base-page-bg → --base-subtle-bg → --base-main-bg → --base-elevated-bg
└─ hover / active / disabled ─┘
- page — canvas / backdrop below the content
- subtle — recessed / inset surface (code, wells)
- main — content surface (cards, panels, grid, dropzone)
- elevated — raised areas: headers, toolbars, dropdowns, popovers
- hover — pointer hover on a surface (rows, options)
- active — pressed / selected
- disabled — inert / readonly surface
- inverse — high-contrast surface, used for tooltips
Role-specific surfaces (--base-input-bg, --base-dropdown-bg, --base-tooltip-bg)
default to values in this scale but can be retargeted on their own — that's the
whole point of keeping them as distinct tokens.
--base-* is the single knob. This package is the canonical contract; its
consumers never define a parallel source of truth, they derive from it:
@keenmate/pure-cssmirrors this token list into SCSS ($base-*), adds theme derivation + its own--pc-*foundation tokens, and ships the grid / utilities / app-shell.@keenmate/pure-admin-corebuilds its component tokens (--pc-*) on top.- KeenMate web components read
--base-*directly (with inline fallbacks).
Every --pc-* is wired as var(--base-*, <fallback>), so overriding one
--base-* re-themes pure-admin components and the web components together.
Traced end-to-end for the accent:
LAYER 0 contract (THIS file, CSS) :root { --base-accent-color: #0ea5e9 }
│ pure-css mirrors the list into SCSS
LAYER 1 pure-css source (SCSS) $base-accent-color: #0ea5e9 !default; ◀─ a THEME overrides here
│ derive
LAYER 2 pure-css framework var (SCSS) $accent-color: $base-accent-color; (serves only as the build fallback)
│ emit (two mixins)
LAYER 3 pure-css emit ──▶ CSS --base-accent-color: #0ea5e9; ◀─ RAIL A · the knob
--pc-accent: var(--base-accent-color, #0ea5e9) ◀─ RAIL B · pure-admin's token
│
LAYER 4 pure-admin-core (CSS) --pc-accent-light, --pc-accent-hover,
--pc-link-color: var(--pc-accent), …
│
LAYER 5 consumers
pure-admin components background: var(--pc-accent-light);
web components --ms-accent-color: var(--base-accent-color, #3b82f6);
- RAIL A (
--base-accent-color) is the knob; RAIL B (--pc-accent) isvar(--base-accent-color, …), so at runtime it simply is the base value — the<fallback>only fires if RAIL A is ever missing (it isn't, once this file or a theme is loaded). - There is no
$pc-*SCSS variable. Thepclayer is born at emission as a CSS property pointing back at--base-*; nothing to author in SCSS.
| Override… | Where | Effect |
|---|---|---|
--base-accent-color |
any :root / .pc-mode-* / [data-*] scope (runtime) |
everything downstream, live — pure-admin and web components |
$base-accent-color |
pure-css SCSS (build) | the default baked into base.css + every --pc-* fallback |
--pc-accent |
a single --pc-* (runtime) |
pure-admin only — use for a deliberate pure-admin-only divergence |
That middle-less "one knob" row is the whole design: a theme (or a time-of-day
[data-daypart] scope) re-sets --base-* and the entire --pc-* layer re-resolves.
Override any --base-* variable in your own :root (or any scope) — the value
flows into every component:
:root {
--base-accent-color: #e11d48; /* rebrand every component's accent */
--base-main-bg: #fafafa;
--base-border-radius-md: 1; /* rounder corners everywhere */
}Colors are defined with CSS light-dark(),
so they follow the active color-scheme.
-
Automatic — the file sets
color-scheme: light darkon:root, so it follows the operating-system preference out of the box. -
Manual — force a theme on any subtree:
<html data-theme="dark"> <!-- or data-theme="light" -->
or set
color-scheme: light | darkon any element.
Every KeenMate component that draws a checkbox (multiselect options, tree nodes,
grid selection, …) should render it the same way, so the tick/dash lands
identically everywhere. The contract has two knobs — --base-checkbox-scale (box
size) and --base-icon-check-size (glyph size) — and one hard rule: scale the
box, never the glyph via transform.
Canonical method (recommended — crisp at every scale):
.x__checkbox {
/* size the BOX via calc — no transform, no compositing layer */
inline-size: calc(var(--x-checkbox-size) * var(--base-checkbox-scale, 1));
block-size: calc(var(--x-checkbox-size) * var(--base-checkbox-scale, 1));
border: var(--base-border-width) solid var(--base-checkbox-border-color);
border-radius: /* component radius */;
position: relative;
}
.x__checkbox::after { /* the tick / dash */
content: '';
position: absolute;
inset: 0;
background-color: /* checkmark color */;
mask: var(--base-icon-check) no-repeat center / var(--base-icon-check-size, 68%);
}
.x__checkbox--indeterminate::after { mask-image: var(--base-icon-indeterminate); }Because the glyph is masked at center over the full box with no transform,
the browser renders it in normal layout space with sub-pixel anti-aliasing — it
stays optically centred on every row regardless of fractional row heights.
Why not transform: scale()? It promotes the element to its own compositing
layer; the layer rasterizes and pixel-snaps the mask. The glyph sits at a
fractional offset (--base-icon-check-size: 68% of a 16px box ≈ 2.56px), so the
snap rounds left on some rows and right on others → the tick drifts, visibly and
inconsistently. Sizing the box with calc() avoids the layer entirely.
Choosing a method for the situation — the same contract, best tool per case:
| need | method |
|---|---|
default / any --base-checkbox-scale |
calc() box sizing above — no transform (crisp) |
| the 1px border must thicken proportionally when scaled up | transform: scale(var(--base-checkbox-scale)) — accept the snapping, or pair with integer sizes |
| avoid fractional glyph offsets entirely | give the glyph an integer --base-icon-check-size (e.g. 11px) and an integer box size |
Naming is intentionally not 1:1 between the base layer and component variables.
For example --ms-primary-bg reads --base-hover-bg, and --drp-primary-bg reads
--base-main-bg. Always theme via the --base-* variables listed here.
These tables are the complete --base-* contract, mirrored token-for-token by
@keenmate/pure-css ($base-*). Light values are Corporate light mode, dark are
Corporate dark mode.
primary is the canonical role name; accent is the legacy alias each --base-primary-*
points back to via var(). Brand blue, kept across light / dark.
| Variable | Purpose |
|---|---|
--base-accent-color |
Primary brand / action color |
--base-accent-color-hover |
Accent hover state |
--base-accent-color-active |
Accent active / pressed state |
--base-accent-color-light |
Subtle accent tint for backgrounds |
--base-accent-color-light-hover |
Subtle accent tint, hover |
--base-primary-color |
Canonical alias → --base-accent-color |
--base-primary-color-hover |
Alias → --base-accent-color-hover |
--base-primary-color-active |
Alias → --base-accent-color-active |
--base-primary-color-light |
Alias → --base-accent-color-light |
--base-primary-color-light-hover |
Alias → --base-accent-color-light-hover |
| Variable | Purpose |
|---|---|
--base-secondary-color |
Neutral grey secondary action color |
--base-secondary-color-hover |
Secondary hover state |
Elevation ladder: page (canvas) < subtle (recessed) < main (content) <
elevated (raised). hover / active / disabled are the interaction-state axis.
| Variable | Purpose |
|---|---|
--base-page-bg |
Canvas / backdrop below the content |
--base-main-bg |
Content surface — cards, panels, grid |
--base-subtle-bg |
Recessed / inset surface (code, wells) |
--base-elevated-bg |
Raised surface — headers, toolbars, popovers |
--base-inverse-bg |
Inverse surface (fallback for tooltip background) |
--base-overlay-bg |
Modal / overlay scrim |
--base-shadow-color |
Shadow tint for elevation (used by the shadow scale) |
--base-hover-bg |
Hover state for any surface (option / row hover) |
--base-active-bg |
Active / pressed surface |
--base-disabled-bg |
Disabled / readonly surface |
| Variable | Purpose |
|---|---|
--base-text-color-1 |
Headers, titles, high-emphasis |
--base-text-color-2 |
Body text, labels |
--base-text-color-3 |
Secondary content, subtitles |
--base-text-color-4 |
Hints, placeholders, captions |
--base-text-color-on-accent |
Text on accent backgrounds |
--base-text-inverted |
Inverse of main text (on inverse / accent surfaces) |
--base-text-on-primary |
Readable text on the primary fill (→ --base-text-color-on-accent) |
--base-text-on-secondary |
Readable text on the secondary fill |
| Variable | Purpose |
|---|---|
--base-border-width |
Shared stroke width the shorthands compose from |
--base-border-color |
Standard border color |
--base-border |
Full border shorthand (1px solid …) |
--base-checkbox-border-color |
Checkbox border |
| Variable | Purpose |
|---|---|
--base-input-bg |
Input background |
--base-input-color |
Input text color |
--base-input-border-color |
Input border color |
--base-input-border |
Input border (normal) |
--base-input-border-hover |
Input border on hover |
--base-input-border-focus |
Input border on focus |
--base-input-placeholder-color |
Placeholder text |
--base-input-bg-disabled |
Disabled input background |
--base-input-clear-color |
Clear (✕) button color |
--base-input-clear-bg-hover |
Clear button background on hover |
| Variable | Purpose |
|---|---|
--base-dropdown-bg |
Dropdown / popover background |
--base-dropdown-border |
Dropdown border |
--base-dropdown-box-shadow |
Dropdown shadow |
| Variable | Purpose |
|---|---|
--base-tooltip-bg |
Tooltip background |
--base-tooltip-text-color |
Tooltip text |
--base-tooltip-color |
Tooltip text alias (web-grid) |
Role ∈ success / danger / warning / info. Each role defines the full set below.
| Variable | Purpose |
|---|---|
--base-<role>-color |
Role fill identity (vivid) |
--base-<role>-bg |
Solid role fill (= -color) |
--base-<role>-color-hover |
Role fill, hover |
--base-<role>-bg-light |
Subtle role tint (light fill) |
--base-<role>-bg-subtle |
Even fainter role tint |
--base-<role>-border |
Role border tint |
--base-<role>-text |
Role as foreground on a light surface (text / links) |
--base-<role>-text-light |
Lighter role foreground variant |
--base-text-on-<role> |
Readable text on the role fill |
| Variable | Purpose |
|---|---|
--base-font-family |
Font stack (sans) |
--base-font-family-mono |
Monospace font stack |
--base-font-size-2xs … 2xl |
Font sizes (unitless multipliers): 2xs xs sm base lg xl 2xl |
--base-font-weight-normal / medium / semibold / bold |
Font weights (400 / 500 / 600 / 700) |
--base-line-height-tight / normal / relaxed |
Line heights |
--base-list-bullet-type |
Default ul, ol marker (disc / circle / square / none / decimal / …) |
| Variable | Purpose |
|---|---|
--base-border-radius-sm / md / lg |
Corner radii (unitless multipliers) |
--base-checkbox-scale |
Checkbox box scale (multiplies the box size; 1 = base). See Checkbox render method |
--base-input-size-xs…xl-height |
Standard input heights (unitless multipliers) |
--base-rem |
Rem base (1rem) the unitless multipliers combine against |
Unitless multipliers, combined as calc(var(--base-space-*) * var(--base-rem)).
| Variable | Purpose |
|---|---|
--base-space-xs / sm / md / base / lg / xl / 2xl |
Spacing scale steps (0.4 → 4.8) |
Colored from --base-shadow-color so shadows theme with the surface.
| Variable | Purpose |
|---|---|
--base-shadow-sm |
Small elevation shadow |
--base-shadow-md |
Medium elevation shadow |
--base-shadow-lg |
Large elevation shadow |
| Variable | Purpose |
|---|---|
--base-duration-fast / normal / medium / slow |
Transition durations (0.1s → 0.3s) |
--base-ease-standard |
Standard easing cubic-bezier(0.4, 0, 0.2, 1) |
--base-ease-out |
Ease-out cubic-bezier(0, 0, 0.2, 1) |
--base-ease-in |
Ease-in cubic-bezier(0.4, 0, 1, 1) |
Generic stacking tiers for overlay coordination.
| Variable | Purpose |
|---|---|
--base-z-dropdown |
Dropdown (7500) |
--base-z-modal-backdrop |
Modal backdrop (6000) |
--base-z-modal |
Modal (7000) |
--base-z-popover |
Popover (7600) |
--base-z-toast |
Toast (8000) |
--base-z-tooltip |
Tooltip (9000) |
Inline data:image/svg+xml Lucide glyphs, rendered by consumers via
mask: var(--base-icon-*); background: currentColor. Mode-invariant — override any
one to re-glyph every consumer at once.
| Variable | Glyph / purpose |
|---|---|
--base-icon-chevron |
Chevron (points right; rotate 90° when open) |
--base-icon-caret-down |
Static down caret (sort / select) |
--base-icon-caret-up |
Static up caret |
--base-icon-close |
✕ close |
--base-icon-clear |
Field-clear — alias → --base-icon-close |
--base-icon-remove |
Item take-out (non-destructive) — alias → --base-icon-close |
--base-icon-expand |
Expand (+ / plus) |
--base-icon-collapse |
Collapse (− / minus) |
--base-icon-add |
Add (plus) |
--base-icon-edit |
Edit (pencil) |
--base-icon-delete |
Delete (destructive trash) |
--base-icon-search |
Search (magnifier) |
--base-icon-filter |
Filter (funnel) |
--base-icon-refresh |
Refresh (two curved arrows; consumers spin it) |
--base-icon-check |
Selected (tick) |
--base-icon-check-size |
Mask-size of the selection glyph in its box (68%) |
--base-icon-indeterminate |
Partially selected (tri-state minus) |
--base-icon-copy |
Copy to clipboard (two sheets) |
--base-icon-ellipsis |
More / overflow (three dots; vertical = same glyph rotated 90°) |
--base-icon-save |
Save (floppy disk) |
--base-icon-settings |
Settings (cog / gear) |
--base-icon-bell |
Notification bell |
--base-icon-user |
User / profile (person) |
--base-icon-download |
Download / export (tray + down arrow) |
--base-icon-link |
Hyperlink / attach-URL (chain) |
--base-icon-external-link |
Opens in a new tab (diagonal arrow out of box) |
--base-icon-info |
Status: info (circle-i) |
--base-icon-success |
Status: success (circle-check) |
--base-icon-warning |
Status: warning (triangle-alert) |
--base-icon-danger |
Status: danger (circle-x) |
Nine brand palette slots (mode-invariant) with paired contrast text, used for categorical color (charts, tags, avatars).
| Variable | Purpose |
|---|---|
--base-color-1 … 9 |
Palette slots 1–9 (amber, pink, emerald, sky, violet, indigo, slate, corporate-blue, dark-slate) |
--base-color-1-text … 9-text |
Readable text paired with each slot |
Unitless multipliers: font sizes, radii, input heights and the spacing scale are stored as plain numbers and combined by components against
--base-rem— e.g.calc(var(--base-font-size-base) * var(--base-rem)). This keeps sizing consistent across all KeenMate components while remaining scalable.
@keenmate/theme-designer— visual theme designer & generator that produces--base-*values from 3 input colors.
MIT © KeenMate