Theming
A thin overlay on shadcn theming with easy-to-override color tokens
Zeno's theme is a thin overlay on shadcn theming. It uses the
exact same semantic CSS-variable tokens (--background, --foreground, --primary, --radius,
and so on); Zeno only ships a set of default values, each mapped to a Tailwind color variable so
every token is easy to override.
Install it as part of setup.
The default lives in packages/ui/src/styles/theme.css and is imported by the shared package's
globals.css. Every app then pulls that in through its own stylesheet:
@import "@{yourWorkspace}/ui/styles/globals.css";Because of the cascade, tokens redeclared after that import win.
Override globally
Each token points at a Tailwind --color-* variable, so restyling is a one-line change per token.
Edit the values in the shared theme, or append an override block after it:
:root {
--primary: var(--color-blue-600);
--primary-foreground: var(--color-white);
--radius: 0.5rem;
}
.dark {
--primary: var(--color-blue-400);
--primary-foreground: var(--color-blue-950);
}A token can point at any Tailwind color (--color-*) or a raw value (oklch(...), #hex). The
radius scale (--radius-sm through --radius-4xl) derives from --radius, so one value rescales
every rounded corner. For the full token list and conventions, see
shadcn theming.
Override in a single app
To restyle one app without touching the shared default, redeclare the tokens after the import in that app's stylesheet. These values win only inside that app; every other consumer keeps the shared theme:
@import "@{yourWorkspace}/ui/styles/globals.css";
:root {
--primary: var(--color-emerald-600);
--primary-foreground: var(--color-white);
}
.dark {
--primary: var(--color-emerald-400);
}Use the shared theme for tokens the app should inherit, and only list the ones it changes.
Fonts
The theme ships two font tokens, --font-text (body) and --font-title (headings).
Tailwind turns each into a utility (font-text, font-title); you just supply the fonts they point at.
Load them per app with next/font,
giving each the matching CSS variable:
import { Aleo, Inter } from "next/font/google"
import { cn } from "@{yourWorkspace}/ui/utils"
const textFont = Inter({ subsets: ["latin"], variable: "--font-text" })
const titleFont = Aleo({ subsets: ["latin"], variable: "--font-title" })
export default function Layout({ children }: LayoutProps<"/">) {
return (
<html
className={cn(textFont.variable, titleFont.variable)}
suppressHydrationWarning
>
<body>{children}</body>
</html>
)
}Use .variable (not .className): it exposes each font as the CSS variable the theme's tokens read,
which is what lets a single <html> carry both fonts at once.
Now you can apply them anywhere with the utility classes:
<h1 className="font-title">Zeno</h1>
<p className="font-text">A thin overlay on shadcn.</p>To make them the app-wide default instead of opting in per element, set them in the base layer:
@layer base {
body {
@apply font-text;
}
h1, h2, h3, h4, h5, h6 {
@apply font-title;
}
}Dark mode
Zeno uses the .dark class strategy. To add a theme toggle, follow shadcn's
Next.js dark mode guide.