Skip to content
Blume
Esc
↑↓navigate↵open⌘Jpreview
On this page

Theming

Tune the look with a handful of config tokens, override any CSS variable in theme.css, or drop down to Tailwind utilities for custom components.

Blume’s theme is token-driven and works in light and dark mode out of the box. Reach for as little or as much as you need: a few config tokens for the common cases, a theme.css to override any design token, or Tailwind utilities for custom components.

Config tokens

The everyday knobs live under theme in your config:

theme: {
  accent: "teal",   // a named preset or any CSS color
  radius: "md",     // none | sm | md | lg
  mode: "system",   // system | light | dark
  fonts: {          // self-hosted Google Fonts
    display: "inter",
    body: "inter",
    mono: "ibm-plex-mono",
  },
}

Accent

The accent color tints interactive and highlighted elements — step markers, active tabs, badges, card hovers, and more. Use a named preset or any CSS color:

theme: {
  accent: "#ff0066", // hex, oklch(), rgb()… anything CSS understands
}

Named presets: blue (default), green, orange, pink, purple, red, and teal. Each preset has a darker light-mode shade and a lighter dark-mode shade, so accent text and button labels meet WCAG AA contrast (4.5:1) in both modes.

A string applies to both color modes; pass an object for a different accent per mode.

A value that’s neither a preset nor a CSS color, like "deep purple" or a hex value without its #, warns BLUME_THEME_COLOR_INVALID at the line in blume.config.ts that sets it, since browsers ignore the styles that use it. The same check covers action, background, and the Open Graph card’s seo.og.palette. Custom properties (var(--brand)), color-mix(), and the other color functions pass as written.

Text on an accent or action fill, like a button label or a step number, is white while white meets AA on that color, and dark otherwise. A light accent gets dark labels without any extra config. blume audit checks a custom accent, action, or background against the same bar.

Radius

radius sets the corner rounding shared by cards, code blocks, callouts, and inputs — none, sm, md (default), or lg.

Color mode

mode sets the initial color scheme:

  • system (default) — follow the reader’s OS preference
  • light / dark — default to one scheme

A toggle in the header always lets readers switch, and their choice is remembered across visits. Dark mode is applied with a data-theme="dark" attribute on the <html> element.

Fonts

fonts sets the typefaces for three roles:

  • display — headings (h1–h6)
  • body — body text, UI, and prose
  • mono — code blocks and inline code

Headings get display-grade letter-spacing (-0.05em) from the theme itself, so whatever you pick for display — including text families like the Inter default — reads correctly at heading sizes instead of depending on tracking built into the font.

Each defaults to a curated Google Font, so Blume looks intentional out of the box:

theme: {
  fonts: {
    display: "inter",         // default
    body: "inter",            // default
    mono: "ibm-plex-mono",    // default
  },
}

Set only the roles you want to change — the rest keep their defaults:

theme: {
  fonts: { display: "geist" }, // body + mono stay Inter / IBM Plex Mono
}

Fonts are self-hosted: Blume downloads them at build time and serves them from your own site, so there’s no runtime request to Google and no layout shift (Astro generates fallback-metric faces automatically).

A bare string is a Google Fonts slug from the curated set below:

Category Slugs
Sans dm-sans figtree geist ibm-plex-sans inter inter-tight manrope open-sans plus-jakarta-sans roboto source-sans-3 space-grotesk work-sans
Serif ibm-plex-serif lora merriweather playfair-display source-serif-4
Mono fira-code geist-mono ibm-plex-mono jetbrains-mono roboto-mono source-code-pro space-mono

Any provider family

Need a family that isn’t in the curated set — say, one that covers a non-Latin script? Pass an object with the family’s exact name. It’s self-hosted and optimized the same way:

theme: {
  fonts: {
    display: { name: "Noto Sans JP", weights: [400, 700] },
    body: { name: "Noto Sans JP", weights: [400, 500, 700] },
  },
}
  • name — the family name exactly as the provider lists it.
  • provider — where the family comes from: google (default), fontsource, bunny, or fontshare.
  • weights — the weights to load, as numbers or a variable range like "100..900". Defaults to [400, 500, 600, 700].
  • subsets — the character subsets to load, by the provider’s names (latin, latin-ext, vietnamese, cyrillic, greek, …). Defaults to latin plus whatever your configured locales need — see below.
  • fallback — the system stack shown while the font loads and for missing glyphs: sans, serif, or mono. Defaults to mono for the mono role and sans otherwise.

Subsets and locales

Google, Bunny, and Fontsource split each family into per-script subsets, and only the subsets you load get a @font-face. Blume derives the list from your i18n.locales: a site with Vietnamese, Polish, Russian, or Greek locales loads vietnamese, latin-ext, cyrillic, or greek alongside latin, so diacritics and non-Latin letters render in your chosen font instead of the system fallback. Sites without an i18n block, or with only Latin-1 languages, load latin alone. Browsers download a subset only when a page uses its characters, and preloads follow the same list.

Set subsets on a family to override the derived list — say, for a single-locale site whose content still needs a script its locale doesn’t imply:

theme: {
  fonts: {
    body: { name: "Be Vietnam Pro", subsets: ["latin", "vietnamese"] },
  },
}

Curated slugs like inter follow the locale-derived list; use the object form to pin subsets for those families too.

Local font files

For a font you own (or one no provider serves), point a role at font files in your project. Each variant becomes one @font-face:

theme: {
  fonts: {
    display: {
      name: "Berkeley Mono",
      variants: [
        { src: "./fonts/BerkeleyMono-Regular.woff2", weight: 400 },
        { src: "./fonts/BerkeleyMono-Bold.woff2", weight: 700 },
      ],
    },
  },
}

Paths resolve from the project root. weight and style (normal, italic, oblique) are optional — Astro reads them from the font file when omitted.

Want to drop back to the system stack? Override the --blume-font-* tokens directly in theme.css.

Dark-mode colors

accent and background follow one rule: a string applies to both color modes, and a { light, dark } object sets each mode individually:

theme: {
  accent: { light: "blue", dark: "teal" },
  background: {
    light: "#ffffff",
    dark: "#0a0a0a",
  },
}

Each color takes a named preset or any CSS color. For background (and backgroundImage) either key can be omitted to override a single mode — background: { dark: "#0a0a0a" } keeps the default light background.

Action color

action is a secondary accent for primary calls to action and the action Tailwind utilities (bg-action, text-action). It defaults to your accent:

theme: {
  action: "#ff0066",
}

Background image

Set a background image behind your content with backgroundImage — a URL or a path under public/. Like the colors, a string applies to both modes and a { light, dark } object sets each mode’s image:

theme: {
  backgroundImage: {
    light: "/bg-light.svg",
    dark: "/bg-dark.svg",
  },
}

theme.css

Drop a theme.css in your project root to override any design token. It’s the last layer in the cascade, so it wins over the defaults and config tokens:

:root {
  --blume-accent: oklch(0.68 0.14 180);
  --blume-radius: 0.5rem;
}

:root[data-theme="dark"] {
  --blume-background: oklch(0.16 0 0);
}

Set a token under :root for light mode and under :root[data-theme="dark"] for dark mode. Color tokens have distinct built-in dark values declared at the dark selector’s higher specificity, so a :root-only override of --blume-accent, --blume-background, and friends applies to light mode only — declare the dark block too when both modes should change.

theme.css is inlined into the site’s Tailwind entry, so Tailwind directives work in it too. The one to know for a monorepo is @source: Blume scans your project for utility classes, and a page that imports components from a sibling workspace package needs that package scanned as well. Point at it relative to theme.css — the standard Tailwind rule — and Blume carries the path into the generated sheet:

@source "../../packages/ui/src";

Design tokens

Token Controls
--blume-background Page background
--blume-foreground Body text
--blume-muted Subtle surfaces — callouts, table headers
--blume-muted-foreground Secondary text
--blume-border Borders and dividers
--blume-accent Accent color
--blume-accent-foreground Text and icons on an accent background
--blume-action Secondary accent (defaults to accent)
--blume-action-foreground Text and icons on an action background (defaults to accent foreground)
--blume-code-background Code block surface
--blume-code-highlight, --blume-code-highlight-border Background and left rule of a highlighted code line (// [!code highlight] or a {1,4-5} range)
--blume-code-add, --blume-code-add-border Background and left rule of an added line (// [!code ++])
--blume-code-remove, --blume-code-remove-border Background and left rule of a removed line (// [!code --])
--blume-code-error, --blume-code-error-border Background and left rule of an error line (// [!code error])
--blume-code-warning, --blume-code-warning-border Background and left rule of a warning line (// [!code warning])
--blume-code-word, --blume-code-word-border Background and outline of a highlighted word (// [!code word:…])
--blume-content-width Max width of the prose column — article, breadcrumb, table of contents, feedback, and pagination (default 42rem)
--blume-radius Corner radius
--blume-font-display Heading font
--blume-font-body Body / UI font
--blume-font-mono Code font

Set a --blume-font-* token to any font stack to use a font outside the curated list, or to fall back to the system stack:

:root {
  --blume-font-body: ui-sans-serif, system-ui, sans-serif;
}

Tailwind utilities

Blume’s theme is built with Tailwind v4 internally, and your project’s .astro, .jsx, .mdx, .ts, and .tsx files are scanned too — so you can style custom components and pages with utility classes, no Tailwind setup required. Every token is exposed as a utility, so your components track the theme automatically:

Token Utilities
--blume-background bg-background
--blume-foreground text-foreground
--blume-muted bg-muted
--blume-muted-foreground text-muted-foreground
--blume-border border-border
--blume-accent bg-accent, text-accent
--blume-accent-foreground text-accent-foreground
--blume-action bg-action, text-action
--blume-action-foreground text-action-foreground
--blume-code-background bg-code
--blume-content-width max-w-content
--blume-radius rounded-blume
--blume-font-display font-display
--blume-font-body font-sans
--blume-font-mono font-mono

Cascade order

Styles resolve in three layers, each overriding the last:

Base

Blume’s reset, default tokens, and component styles.

Config tokens

--blume-accent, --blume-radius, and the --blume-font-* tokens from theme.

theme.css

Your token overrides — the final word.

Last updated on

Was this page helpful?