Zeno

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:

apps/{yourApp}/src/app/globals.css
@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:

packages/ui/src/styles/theme.css
: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:

apps/{yourApp}/src/app/globals.css
@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:

apps/{yourApp}/src/app/layout.tsx
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:

apps/{yourApp}/src/app/globals.css
@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.