Docs / Styling & UI / Fonts, colors & dark mode
Fonts, colors & dark mode
Keel ships a three-font system: Hanken Grotesk for UI text and headings, JetBrains Mono for code, labels, and the small uppercase "eyebrow" text above section headings, and Instrument Serif for the rare italic accent word on marketing pages. All three are next/font/google calls in app/layout.tsx, self-hosted and subset at build time, no external font request at runtime.
How a font reaches Tailwind
Each font is loaded with a CSS variable name (--font-hanken-grotesk, etc.), then app/globals.css's @theme inline block maps those onto Tailwind's own font tokens:
--font-sans: var(--font-hanken-grotesk);
--font-mono: var(--font-jetbrains-mono);
--font-serif: var(--font-instrument-serif);
--font-heading: var(--font-hanken-grotesk);
So font-sans, font-mono, and font-serif in any className resolve to the right typeface, and the mapping lives in exactly one place.
Swapping a font
Change the next/font/google import and its variable name in app/layout.tsx, then update the matching line in globals.css's @theme inline block, both have to change together, the variable name is the only link between them. Using a font that isn't on Google Fonts works the same way with next/font/local instead.
Instrument Serif is intentionally rare
Per Keel's design rules, the serif accent is budgeted at roughly three uses per marketing page (the serifAccent class in components/marketing/shared.ts), one italicized word in a heading, not a general display font. Swap it for a different accent face if you like, but keep the same "sparse, one word" usage or it stops reading as an accent.
Colors and other theme tokens
Every color (--color-accent, --color-accent-deep, --ink-2, --bg-raised, and the rest) is a CSS custom property in app/globals.css, exposed to Tailwind through the same @theme inline mechanism as the fonts. Each one is defined twice: once under :root (light mode) and again under .dark with the same variable names but different values. Change a color by editing its value in both blocks, not by hunting down every class that uses it, every component already reads from the token, not a literal color.
Picking a different color theme
Swapping Keel's olive/terracotta palette for your own means editing the values inside :root and .dark in app/globals.css, the variable names (--accent, --accent-deep, --accent-soft, and so on) and everything that reads them stay the same. Keep the same light/dark pairing discipline: for every color you change under :root, change its matching entry under .dark too, so contrast in dark mode doesn't silently break because only one half got updated.
Removing dark mode
Dark mode is next-themes, wired through components/theme-provider.tsx (a thin client wrapper around ThemeProvider so it can be imported into the server-rendered root layout.tsx) using the class strategy, next-themes toggles a .dark class on <html>, which is what .dark in globals.css and Tailwind's @custom-variant dark rule both key off. To remove it entirely:
- Delete the
.dark { ... }block inapp/globals.css, its:rootvalues become the only theme. - Remove
<ThemeProvider>fromapp/layout.tsxand the<ThemeToggle />button fromcomponents/marketing/site-header.tsx(and anywhere else it's used, like the dashboard shell). dark:utility classes elsewhere in the codebase become dead weight, not broken, Tailwind just never matches them since nothing ever adds the.darkclass. Safe to leave them or strip them later, whichever you prefer.