native-dark
v0.2.0
Published
CSS-only UI library — dark-first design tokens and BEM primitives ported from the Emmerse dashboard.
Readme
Native Dark CSS
A CSS-only UI primitives library with first-class light and dark themes. Hand-written, framework-agnostic.
Install
pnpm add native-darkOr drop the bundled file in directly:
<link rel="stylesheet" href="node_modules/native-dark/dist/native-dark.css" />Fonts
Fonts are consumer-controlled: the library exposes two tokens, --nd-font-sans and --nd-font-mono, and never fetches a web font itself. Defaults are Geist / Geist Mono with strong system fallbacks (ui-sans-serif, system-ui, …).
To use Geist, load it however you normally load web fonts:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" />To use anything else, override the tokens:
:root {
--nd-font-sans: 'Inter', system-ui, sans-serif;
--nd-font-mono: 'JetBrains Mono', ui-monospace, monospace;
}Dark / Light Theme
Theme is controlled by the data-theme attribute on any ancestor (typically <html>):
<html data-theme="dark">
…
</html>Omit the attribute (or set anything other than "dark") for light. Switching at runtime is a single attribute write; nothing in the library reads the OS preference, so you decide the policy.
Class API
All classes use BEM. State is conveyed by:
- standard pseudo-classes (
:disabled,:checked,:focus-visible) - ARIA attributes (
[aria-selected="true"]) - explicit state classes (
.is-open,.is-loading)
See demo for more.
Tokens
Primary color
The brand color is exposed as a single token, --nd-primary. Override the variable to set custom theme color:
:root {
--nd-primary: #ff5b8a;
}See demo for more.
Utilities (optional)
An atomic utility layer ships separately and is not part of the main bundle — import it only if you want it:
@import 'native-dark/utilities'; /* or 'native-dark/utilities/min' */<link rel="stylesheet" href="node_modules/native-dark/dist/native-dark-utilities.css" />All classes are namespaced with nd- so they never collide with the BEM component classes:
<div class="nd-flex nd-items-center nd-justify-between nd-gap-3 nd-p-4">…</div>
<ul class="nd-space-y-2">…</ul>
<div class="nd-grid nd-grid-cols-1 nd-md:grid-cols-3 nd-gap-4">…</div>What's included: padding / margin (nd-p-*, nd-px-*, nd-mt-*, negative nd--mt-*, nd-mx-auto), flexbox (nd-flex, nd-items-*, nd-justify-*, nd-flex-1, nd-grow…), grid (nd-grid, nd-grid-cols-1..12, nd-col-span-*…), gap & space-between (nd-gap-*, nd-space-x-*, nd-space-y-*), and a few display helpers (nd-block, nd-hidden…).
Spacing scale
Spacing uses integer steps tokenised as --nd-space-* (rem-based, 1 = 0.25rem). Retheme spacing globally by overriding a token:
:root {
--nd-space-4: 12px; /* every nd-p-4, nd-gap-4, nd-mt-4, … follows */
}Responsive
Mobile-first min-width variants are prefixed sm: / md: / lg: (480px / 721px / 1024px):
<div class="nd-hidden nd-md:block">visible from 721px up</div>The utility layer is generated by scripts/generate-utilities.mjs (pnpm gen:utilities) — edit that config, not src/utilities.css.
Build
pnpm install
pnpm buildDemo
pnpm demoThen open http://localhost:4321/demo/.
Browser support
Evergreen browsers only. Uses color-mix(), oklch(), oklch(from ...) relative color syntax, light-dark(), @property, backdrop-filter, dvh, text-wrap: balance.
Note No IE / legacy fallbacks.
Viewport support
Mobile-first. Minimum supported viewport: 375 × 667 (iPhone SE class).
