Themes
Six complete personalities, each in light and dark. Pick one at install time, or let people switch.
A theme changes everything about how the components look: colors, fonts, corner radius, shadows, spacing, how buttons and inputs are shaped, and whether presses ripple. The components and their APIs never change, so an app can switch themes without touching its code.
Click a card, or use the switcher in the sidebar. The whole site, every preview included, follows.
How a theme is built
Every theme is three layers:
| Layer | Lives in | What it decides |
|---|---|---|
| Tokens | packages/tokens/src/themes/<name>.ts | Colors (light and dark), fonts, base radius, the three elevation shadows, the brand gradient, ripple on or off |
| Structure | packages/ui/src/themes/<name>/structure.css | Component shapes and spacing. Generated from one of shadcn's styles (Vega, Maia, Lyra, Luma, Sera...), except Zynactra, which is hand-tuned |
| Personality | packages/ui/src/themes/<name>/personality.css | A few hand-written touches: gradient CTAs for Sunset, serif titles for Atelier, mono labels for Volt |
A theme can also wear the Zynactra signature, the motion and detail pack, with "signature": true in registry.json. Zynactra does; the others don't.
| Theme | Structure | Feels like |
|---|---|---|
| Zynactra | Hand-tuned (from Nova) | Square, warm, flat, precise |
| Sunset | Maia: pill shapes, generous spacing | Zynactra's original website |
| Harbor | Vega: the classic shadcn look | Calm, professional SaaS |
| Grove | Luma: very round, soft shadows | Friendly consumer app |
| Atelier | Sera: tracked capitals, underline fields | Editorial, premium |
| Volt | Lyra: sharp, boxy | Developer tooling |
Choosing themes for an app
Themes are plain CSS. Import the ones the app uses; nothing else is shipped.
/* One theme: */
@import "@zynactra/ui/core.css";
@import "@zynactra/ui/themes/harbor.css";/* Zynactra by default, plus the themes people can switch to: */
@import "@zynactra/ui/styles.css"; /* core + Zynactra */
@import "@zynactra/ui/themes/grove.css";
@import "@zynactra/ui/themes/atelier.css";Use @zynactra/ui/themes/all.css to include every theme.
Setting the theme
Fixed theme. Set it on <html> and you're done:
<html data-theme="harbor">Switchable theme. Add the script (it applies the saved theme before the first paint, so nothing flashes) and the provider:
import { ZynactraThemeProvider, ZynactraThemeScript } from "@zynactra/ui/theme"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<ZynactraThemeScript defaultTheme="harbor" themes={["harbor", "grove"]} />
</head>
<body>
<ZynactraThemeProvider defaultTheme="harbor" themes={["harbor", "grove"]}>
{children}
</ZynactraThemeProvider>
</body>
</html>
)
}"use client"
import { useZynactraTheme } from "@zynactra/ui/theme"
export function ThemePicker() {
const { theme, setTheme, themes } = useZynactraTheme()
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
{themes.map((t) => (
<option key={t.name} value={t.name}>{t.label}</option>
))}
</select>
)
}Light and dark
Mode is separate from theme: every theme has both. Add the dark class (or data-mode="dark") to <html>. next-themes with attribute="class" handles that, including following the system setting.
Fonts
Each theme names its fonts; the app loads them. Self-host with Fontsource:
| Theme | Packages |
|---|---|
| Zynactra, Sunset | @fontsource/instrument-sans, @fontsource/jetbrains-mono |
| Harbor | @fontsource-variable/inter |
| Grove | @fontsource-variable/plus-jakarta-sans |
| Atelier | @fontsource-variable/fraunces, @fontsource-variable/inter |
| Volt | @fontsource-variable/space-grotesk, @fontsource/jetbrains-mono |
Adding a theme
pnpm theme:new ember --from harbor --structure mira --label "Ember"- That copies Harbor's tokens to
packages/tokens/src/themes/ember.tsand registers the theme. Edit its colors for light and dark, fonts, radius, shadows and ripple, and write its one-line description inregistry.json. pnpm sync:upstreamgenerates its structure from the shadcn style you picked, a personality stub and its CSS entry point.- Add character in
packages/ui/src/themes/ember/personality.cssif the structure alone isn't enough. Set"signature": trueinregistry.jsonif it should move like Zynactra. pnpm check, then select it in the docs switcher and check every page in light and dark.
Structures to start from: vega (classic), nova (compact), maia (soft, pill), lyra (sharp, boxy), mira (dense), luma (very round), sera (editorial), rhea (rounded, soft cards).
Contrast is your job: body text needs 4.5:1 on its background, and text on brand fills needs 4.5:1 against every color of the brand-fill gradient.