@broberg/theme
v0.3.1
Published
Headless design-token theme store + neutral CSS baseline (Tailwind v4) for the broberg.ai estate. Framework-agnostic core + thin React/Preact adapters + a DESIGN.md → Tailwind v4 generator.
Readme
@broberg/theme
The single source of truth for how every app in the broberg.ai estate looks — a framework-agnostic theme store plus a neutral shadcn/ui-compatible CSS token baseline. Flip light / dark / warm / cool the same way everywhere; rebrand from one place.
Two halves, one package — adopt as much as your stack supports:
- JS theme store (this npm package) — sets
data-themeon<html>, persists tolocalStorage, notifies subscribers. Works in any app (React, Preact, vanilla; Tailwind or not). SSR-safe.- CSS token baseline (
css/neutral-preset.css) — copy-owned. Requires Tailwind v4 (it uses@theme, which cannot be@imported from node_modules). Non-Tailwind apps use the raw CSS variables directly.
Install
npm i @broberg/theme # or pnpm / bun add1. The CSS baseline (Tailwind v4)
Copy node_modules/@broberg/theme/css/neutral-preset.css into your app's CSS
entry (e.g. globals.css). It ships the neutral token vocabulary, dark-first,
with six named data-theme variants (light, dark, light-cool,
light-warm, dark-cool, dark-warm) and the @theme inline bridge.
Brand override pattern
Override only what makes you you — --primary, --ring, --radius — in
:root (and [data-theme="light"] if your brand color differs per mode):
:root {
--primary: oklch(0.82 0.17 85); /* your brand color */
--ring: oklch(0.82 0.17 85);
--radius: 0.625rem;
}Everything else inherits the neutral baseline, so a new app is on-brand, accessible and dark-mode-ready in three lines.
Responsive & touch tokens
The preset also ships breakpoint tokens (--breakpoint-sm/md/lg/xl =
640/768/1024/1280, in the @theme block so Tailwind v4's sm:/md:/… variants
resolve them) and a touch-target token (--touch-target-min: 44px in :root)
— one source, so every app switches layouts at the same widths and never ships a
sub-44px tap target:
.btn { min-height: var(--touch-target-min); min-width: var(--touch-target-min); }Read the same values in JS (e.g. for matchMedia) from the headless core:
import { BREAKPOINTS, TOUCH_TARGET_MIN } from "@broberg/theme";
if (matchMedia(`(min-width: ${BREAKPOINTS.md}px)`).matches) { /* tablet and up */ }BREAKPOINTS and --breakpoint-* are the same numbers; the DESIGN.md generator
emits both from the breakpoints: / touch: token blocks.
2. The theme store
React / Next.js (Stack A)
import { ThemeProvider, useTheme, ThemeToggle } from "@broberg/theme/react";
// app root
<ThemeProvider defaultTheme="dark" followSystem>
{children}
</ThemeProvider>
// anywhere
const { theme, setTheme, toggleTheme, themes } = useTheme();
<ThemeToggle /> // minimal light<->dark button, data-testid="theme-toggle"useTheme subscribes via useSyncExternalStore — no next-themes dependency,
SSR-safe. The full Sun/Moon/Monitor dropdown is copy-owned per app (build it
from your own design-system components; ThemeToggle is a drop-in starter).
Preact / Bun (Stack B)
import { initTheme } from "@broberg/theme/preact"; // call once in your entry
import { useTheme } from "@broberg/theme/preact";
initTheme({ defaultTheme: "dark", followSystem: true });
const { theme, setTheme, toggleTheme } = useTheme();Vanilla / no framework
import { initTheme, setTheme, toggleTheme, onThemeChange } from "@broberg/theme";
initTheme();
setTheme("dark-warm");API
| Export | Description |
|---|---|
| initTheme(opts?) | Resolve (stored › system › default), apply to <html>, return the key. |
| getTheme() | Current ThemeKey. |
| setTheme(key) | Apply + persist + notify. No-op on invalid keys. |
| toggleTheme() | Cycle light ⇄ dark (variants collapse to their base mode). |
| onThemeChange(fn) | Subscribe; returns an unsubscribe. |
| THEME_KEYS | All six ThemeKeys. |
| BREAKPOINTS | { sm:640, md:768, lg:1024, xl:1280 } — responsive breakpoints (px) for matchMedia. |
| TOUCH_TARGET_MIN | 44 — minimum touch-target size (px). |
InitThemeOptions: { defaultTheme?, followSystem?, storageKey? } (default key
"broberg-theme").
Notes
- Stack target: Tailwind v4 only — no v3 / legacy support by design.
- The headless core imports no framework packages (
tsc --noEmitclean; nonext/*, no React/Preact in@broberg/theme). - Part of the
broberg-ai/componentsmonorepo (F001).
