@nightowlsdev/theme
UIFramework-agnostic design tokens and theme presets for the Night Owls chat/agent UI.
What it does
@nightowlsdev/theme is the pure, dependency-free design-token layer underneath the Night Owls UI. It ships a set of named themes (ink/Vercel, owl/classic, mono/ChatGPT, warm, midnight, paper, 8-bit, tui, and more), each defining light/dark colors, radius, density, and font stacks (the package ships no fonts, only safe fallbacks). Use getTheme/resolveTheme to resolve a theme to CSS variables, defineTheme/extendTheme to author or override one, and the accent/theme presets plus font-scale helpers to tune accent color and density. It has zero React or @mastra dependency, so it can be consumed by any renderer; @nightowlsdev/react re-exports the whole API for convenience.
Install
pnpm add @nightowlsdev/themeKey exports
- themes / themeNames / DEFAULT_THEME / getTheme
- defineTheme / extendTheme
- resolveTheme / resolveThemeName
- accentPresets / themePresets / applyPreset
- fontSizes / FONT_SCALES / resolveFontScale
- built-in themes: ink, owl, mono, warm, midnight, compact, paper, eightbit, tui, nightowls
- types: Theme, ThemeColors, ThemeFont, ThemeMode, ThemeVars, Density, FontSize
Usage
import { getTheme, resolveTheme, themeNames } from "@nightowlsdev/theme";
// Resolve a built-in theme to CSS variables for a given mode.
const vars = resolveTheme(getTheme("nightowls"), "dark");
console.log(themeNames); // ["ink", "owl", "mono", "warm", ... "nightowls"]What it provides
theme is the pure, dependency-free design-token layer underneath the whole Night Owls UI. A Theme is plain data — light colors, optional dark overrides, radius, density, and font stacks — and resolveTheme turns it into the flat --owl-* CSS-variable map that any renderer applies on a root element. It ships nine named themes (ink, owl, mono, warm, midnight, compact, paper, 8bit, tui), authoring helpers (defineTheme / extendTheme), a curated accent-preset system, and font-scale helpers, with zero React and zero @mastra.
When to use it
- You want the Night Owls chat/agent look-and-feel's design tokens in a non-React context — a server-rendered stylesheet, a plain CSS build, another framework.
- You want to author a brand theme, or personalize a built-in one (one-click accent presets), and resolve it to CSS variables.
- You are building a bespoke renderer and need the same --owl-* contract the built-in components read, so your UI and the chat share one theme.
When not to
- You are already using @nightowlsdev/react or @nightowlsdev/ui — both re-export / apply this API, so you rarely import theme directly.
- You need actual components (a provider, buttons) rather than tokens — reach for @nightowlsdev/ui (chrome) or @nightowlsdev/react (chat).
Alternatives
- @nightowlsdev/ui (ThemeProvider)You want a React provider that resolves a theme, applies the --owl-* vars + a data-theme attribute on a root element, and persists the user's light/dark/system choice for you.
- @nightowlsdev/reactYou are inside the chat UI — it re-exports the entire theme API, so a single import surface covers both.
- Hand-rolled CSS variables / Tailwind configYou don't want the token contract at all. You lose the named themes, the light/dark merge, and the accent-preset system.
Strengths
- Zero dependencies — no React, no @mastra — so any renderer or plain CSS can consume it.
- Pure functions: resolveTheme is DOM-free and deterministic, so it is SSR-safe and trivially testable.
- A theme is plain data; extendTheme / applyPreset personalize in one call while keeping identity (radius, density, font, and the theme name that keys theme-scoped CSS all survive).
- Ships safe font FALLBACKS only, no bundled webfonts — no weight, no FOUT surprises; you supply the actual @font-face.
Limits & trade-offs
- Tokens only: it does not touch the DOM. Something (an adapter, or you) must put the resolved --owl-* vars on a root element.
- The token vocabulary is fixed — the semantic --owl-* roles (bg/surface/border/text/muted/accent…). A design that needs different roles has to layer its own vars on top.
- Ships no fonts. The stacks end in a safe generic; matching the intended face is on the host.
How it works
A Theme carries light colors, an optional dark partial merged over them, a radius, a density, and font stacks. resolveTheme(theme, mode) merges dark over light for the chosen mode and emits the flat { '--owl-bg': …, '--owl-accent': …, '--owl-radius': … } map (deriving accentSoft via color-mix and defaulting accentText when omitted). defineTheme type-checks a literal; extendTheme deep-merges overrides to derive a new theme. accentPresets + applyPreset recolor only the accent tokens in light and dark, leaving everything else intact. Density and font-size are separate axes (FONT_SCALES / resolveFontScale) an adapter maps to attributes.
Examples
Resolve a built-in theme to CSS variables
getTheme by name, resolveTheme for a mode. themeNames lists the built-ins; DEFAULT_THEME is 'ink'.
import { getTheme, resolveTheme, themeNames } from "@nightowlsdev/theme";
const vars = resolveTheme(getTheme("midnight")!, "dark");
// -> { "--owl-bg": "…", "--owl-accent": "…", "--owl-radius": "…", … }
console.log(themeNames); // ["ink","owl","mono","warm","midnight","compact","paper","8bit","tui"]Author a brand theme, then recolor its accent
extendTheme derives from a base by deep-merge; applyPreset swaps only the accent tokens (light + dark) and keeps radius/density/font.
import { extendTheme, getTheme, applyPreset, accentPresets } from "@nightowlsdev/theme";
const brand = extendTheme(getTheme("ink")!, {
name: "acme",
colors: { accent: "#E8A33D", accentHover: "#cf8f2f" },
radius: "0.75rem",
});
const teal = accentPresets.find((p) => p.name === "teal")!;
const brandTeal = applyPreset(brand, teal); // accent recolored, identity keptApply the resolved vars on a root element
Tokens are framework-agnostic: spread the map onto any element's style and children read the --owl-* vars.
import { getTheme, resolveTheme } from "@nightowlsdev/theme";
export function Themed({ children }: { children: React.ReactNode }) {
const vars = resolveTheme(getTheme("owl")!, "light") as React.CSSProperties;
return <div style={{ ...vars, background: "var(--owl-bg)", color: "var(--owl-text)" }}>{children}</div>;
}Doing the parts it doesn't support
- Apply tokens to the DOM for youtheme is pure. Use @nightowlsdev/ui's <ThemeProvider> or @nightowlsdev/react's <SwarmProvider> to resolve + apply the vars and set data-theme, or spread resolveTheme(...) onto a root element's style yourself.
- Persist a user's light/dark choice / follow the OSNot here — the package is stateless. @nightowlsdev/ui's ThemeProvider does the localStorage persistence and matchMedia 'system' following, SSR-safe.
- A token role the semantic set doesn't haveresolveTheme returns a plain map — spread your own extra --owl-* (or app-specific) variables alongside it, or set the CSS variable directly on the element. ThemeVars is just Record<string,string>.
Related
- react — Re-exports this whole API and applies the tokens in the chat UI.
- ui — A React <ThemeProvider> that resolves + applies these tokens and persists the mode.
- graph-react — Another surface that reads the same --owl-* custom properties (inline SVG, no CSS build).