---
title: Theming
description: 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:

```ts blume.config.ts lineNumbers
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:

```ts blume.config.ts lineNumbers
theme: {
  accent: "#ff0066", // hex, oklch(), rgb()… anything CSS understands
}
```

Named presets: `blue` (default), `green`, `orange`, `pink`, `purple`, `red`, and `teal`.

A string applies to both color modes; pass an object for [a different accent per mode](#dark-mode-colors).

### 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:

```ts blume.config.ts lineNumbers
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:

```ts blume.config.ts lineNumbers
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:

```ts blume.config.ts lineNumbers
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]`.
- **`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.

#### 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`:

```ts blume.config.ts lineNumbers
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.

:::note
When you set `theme.fonts` explicitly, your display and body fonts also style the generated [Open Graph cards](/docs/configuration/seo#card-fonts) automatically, so shared links match the site. Non-Google provider families are skipped there (the card renderer can only fetch from Google Fonts); local files work everywhere.
:::

Want to drop back to the system stack? Override the `--blume-font-*` tokens directly in [`theme.css`](#themecss).

### 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:

```ts blume.config.ts lineNumbers
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`:

```ts blume.config.ts
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:

```ts blume.config.ts lineNumbers
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:

```css theme.css lineNumbers
: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.

### 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-code-background`   | Code block surface                        |
| `--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:

```css theme.css lineNumbers
: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`, `.tsx`, and `.jsx` 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-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:

<Steps>
  <Step title="Base">Blume's reset, default tokens, and component styles.</Step>
  <Step title="Config tokens">
    `--blume-accent`, `--blume-radius`, and the `--blume-font-*` tokens from
    `theme`.
  </Step>
  <Step title="theme.css">Your token overrides — the final word.</Step>
</Steps>
