# Styles

## `styles.colors`

<Parameter name="primary" type="string (any valid CSS color)" default="#6366f1" />

<Parameter name="text" type="string" default="browser default" />

<Parameter name="dark" type="object" default="falls back to the light-mode values above">
  `{ primary?, text? }` — dark-mode overrides.
</Parameter>

```json
{
  "styles": {
    "colors": {
      "primary": "#16a34a",
      "text": "#0f172a",
      "dark": {
        "primary": "#4ade80"
      }
    }
  }
}
```

`primary` drives links, active nav states, buttons, and the topbar's active tab underline. Every site gets a light/dark toggle automatically (top-right of the topbar) — `styles.colors.dark` only needs to specify the values that actually differ in dark mode; anything left unset falls back to the light-mode value.

<Callout type="note">
  Background color isn't set here — see `styles.background` below, the single field for that.
</Callout>

## `styles.logo` and `styles.favicon`

`favicon` is a single path. `logo` accepts either one path used for both color modes, or a `{ light?, dark?, label? }` object — each image shown only while that mode is active:

```json title="Single logo"
{ "styles": { "logo": "/logo.svg" } }
```

```json title="Dual light/dark logo"
{
  "styles": {
    "logo": {
      "light": "/logo-light.svg",
      "dark": "/logo-dark.svg",
      "label": "My Docs"
    }
  }
}
```

Both forms are root-relative paths (`/logo.svg`, not a relative path or a filename alone), and they always match your project's actual folder structure — whatever you'd type to get from your content directory to the file, that's what goes here:

```
my-docs/
├── writedocs.json
└── logo.svg   →  "styles.logo": "/logo.svg"
```

Any folder works the same way — there's no dedicated assets folder to create, `public/` included:

```
my-docs/
├── writedocs.json
└── assets/
    └── logo.svg   →  "styles.logo": "/assets/logo.svg"
```

Write the path that matches where the file actually is, and it resolves — no folder name is special. If you're coming from Astro or a similar tool and already have a `public/` folder, it works too, including its own convention of dropping the `public` segment (`public/logo.svg` → `/logo.svg`) — but that's for familiarity only, never something you need to add or think about specifically.

On the object form, `light` and `dark` are both optional — set just one if you only have an image for one mode, or omit both entirely and set only `label` to override the topbar's fallback text without adding any logo image:

```json title="Label only, no logo image"
{ "styles": { "logo": { "label": "My Docs" } } }
```

<Callout type="note">
  The topbar shows *only* the logo image by default — no site name text next to it, since a real logo asset is usually already a wordmark. `label` (only available on the object form) opts back into showing text next to the image, for a symbol-only mark that needs a name alongside it, or stands alone with no image at all as shown above. With no `styles.logo` set at all, the topbar falls back to `name`.
</Callout>

## `styles.codeblocks`

The [Shiki](https://shiki.style) theme *name* used for fenced code blocks and the API playground's request/response snippets, in light and dark mode:

```json
{
  "styles": {
    "codeblocks": {
      "light": "min-light",
      "dark": "dracula"
    }
  }
}
```

Both default to `github-light`/`github-dark` if omitted. Any theme name from [shiki.style/themes](https://shiki.style/themes) works — this is a built-in theme name, not a custom theme file. Leave either key unset to keep its default while overriding only the other.

### `styles.codeblocks.langAlias`

Maps a fenced (` ``` `) code block's own language tag to whichever Shiki grammar actually highlights it, for a language tag that either doesn't tokenize well under its own grammar, or that you'd rather write under a shorter/friendlier tag than Shiki's own bundled id:

```json
{
  "styles": {
    "codeblocks": {
      "langAlias": {
        "groovy": "java"
      }
    }
  }
}
```

Merged with (not replacing) one built-in default — `mdx` is aliased to `jsx` out of the box, since Shiki's own `mdx` grammar tokenizes an entire fence as one plain run with no tag/attribute distinction, while `jsx` actually highlights the JSX-heavy `<Callout>`/`<Card>`-style snippets this site's own docs use ```` ```mdx ```` for.

## `styles.navbar`

The topbar's own background color, in light and dark mode — independent of `styles.background` (the page's). Each side (`light`/`dark`) is either a plain color string, or an object with an optional `accent` override:

```json
{
  "styles": {
    "navbar": {
      "light": "#f8fafc",
      "dark": "#020617"
    }
  }
}
```

Both default to the page background if omitted — an unconfigured navbar blends into the page exactly as before. Leave either key unset to keep that side matching the page while overriding only the other. The mobile menu's slide-out panel shares this same color, since its header row is designed to read as a continuation of the real topbar.

The moment either side is configured at all — plain string or object, doesn't matter — every text/icon color drawn on top of it (the brand name, product/version switchers, tab labels, topbar links, the search box, the theme toggle) switches from `styles.colors.text` to an automatically-computed black or white, picked by contrast against that background. There's no field to set this yourself: it's always correct, including for the common way to accidentally break it — a brand-colored navbar (the same value as `styles.colors.primary`, or close to it), where `styles.colors.text` alone would otherwise read as near-invisible near-black-on-near-primary text.

`accent` is the one thing that *is* still a real design choice — the underline under the active tab in the navbar (which otherwise uses `styles.colors.primary`, same as everywhere else on the page). Hovering a tab tints its background with the navbar's own text color, so it works on any navbar color with no setting:

```json
{
  "styles": {
    "navbar": {
      "light": { "background": "#6366f1", "accent": "#f59e0b" },
      "dark": { "background": "#6366f1", "accent": "#f59e0b" }
    }
  }
}
```

Leaving `accent` unset keeps the ordinary brand-colored highlight (`styles.colors.primary`) exactly as before — set it only when a navbar needs its own accent independent of that. Either way, the text painted on top of the active tab's own fill is also chosen automatically by contrast, for the same reason the navbar's plain text color is: getting an active-tab's text color wrong is a legibility bug, not a legitimate design choice to expose.

## `styles.background`

The single field for any background color, plus an optional image layered on top of it:

```json
{
  "styles": {
    "background": {
      "colors": {
        "light": "#f8fafc",
        "dark": "#0f172a"
      },
      "images": {
        "light": "/images/light-background.png",
        "dark": "/images/dark-background.png"
      }
    }
  }
}
```

<Parameter name="colors.light" type="string" default="#ffffff" />

<Parameter name="colors.dark" type="string" default="#0b1120" />

<Parameter name="images.light" type="string (path or URL)" default="none" />

<Parameter name="images.dark" type="string (path or URL)" default="none" />

Both `colors` and `images` are optional, and each side (`light`/`dark`) is independently optional too — leave a key unset to keep that side's default while overriding only the other. `images` paths resolve the same way `styles.logo`/`favicon` do (see above) — the literal path from your content directory to the file, no matter which folder it's in.

`colors` drives every flat, opaque surface site-wide — dropdowns, modals, kbd chips, the footer, and the topbar (unless `styles.navbar` overrides it) all use it, same as if it were still the single `styles.colors.background` field this repo used to have before the two were merged into one. `images` shows through the main content, table-of-contents, and sidebar columns — the topbar and footer always paint their own opaque copy of `colors` over themselves, specifically so a background image sits behind your content and navigation, not behind the chrome that frames them.

See `docs.json-examples/00-kitchen-sink/` in this repo for a full working example of `styles.navbar` and `styles.background` together.

## `styles.fonts`

Every writedocs site uses [Inter](https://fonts.google.com/specimen/Inter) by default, genuinely loaded as a web font — not just referenced as a name a reader might happen to already have installed. Set `styles.fonts` to use a different font instead, for your whole site or separately for headings and body text.

### Google Fonts

Set `family` to any [Google Fonts](https://fonts.google.com) name and it loads automatically — no `source` needed:

```json
{
  "styles": {
    "fonts": {
      "family": "Space Grotesk"
    }
  }
}
```

### Headings and body text separately

Override just headings, just body text, or both — whichever isn't overridden falls back to the top-level `family` (or Inter, if you haven't set one):

```json
{
  "styles": {
    "fonts": {
      "family": "Inter",
      "heading": {
        "family": "Playfair Display",
        "weight": 700
      }
    }
  }
}
```

### Local or externally-hosted fonts

Set `source` (and `format`) to use a font file instead of a Google Font — either a path to a file in your own project (resolves the same way `styles.logo`/`styles.background.images` do — see above), or a full URL to a font hosted elsewhere:

```json title="Local font file"
{
  "styles": {
    "fonts": {
      "family": "InterDisplay",
      "source": "/fonts/InterDisplay-Regular.woff2",
      "format": "woff2",
      "weight": 400
    }
  }
}
```

```json title="Externally hosted font"
{
  "styles": {
    "fonts": {
      "family": "Hubot Sans",
      "source": "https://mintlify-assets.b-cdn.net/fonts/Hubot-Sans.woff2",
      "format": "woff2",
      "weight": 400
    }
  }
}
```

<Parameter name="family" type="string" required>
  A Google Fonts name, or the name you're giving a `source` font.
</Parameter>

<Parameter name="weight" type="number">
  e.g. `400`, `700` — narrows which Google Fonts weight loads, or labels a `source` font's own weight.
</Parameter>

<Parameter name="source" type="string (path or URL)">
  Omit to auto-load `family` from Google Fonts. Set to use a local or externally-hosted font file instead.
</Parameter>

<Parameter name="format" type='"woff" | "woff2"'>
  Required together with `source`.
</Parameter>

<Parameter name="heading" type="object">
  Same four fields, applied to headings only. Falls back to the fields above when unset.
</Parameter>

<Parameter name="body" type="object">
  Same four fields, applied to body text only. Falls back to the fields above when unset.
</Parameter>

<Callout type="note">
  `weight` controls both which font file loads *and* how bold it actually renders — set `heading.weight`/`body.weight` (or the top-level `weight`, which both fall back to) and it's applied as `font-weight` on your headings/body text too, not just used to pick a file. Leave `weight` unset entirely and headings/body text keep the plain browser-default weight (bold headings, normal body text) exactly as before you configured `styles.fonts` at all.
</Callout>

See `docs.json-examples/00-kitchen-sink/` for a working `family` + `heading` override.