Theming

Syntext sites are customizable to match your brand. This guide covers colors, fonts, and advanced styling options.

Theme Presets

A theme is data, not code. Pick a preset by name, then override individual tokens — nothing needs forking.

{
  "theme": "default",
  "themeOverrides": {
    "colors": { "light": { "accent": "#077155" } }
  }
}
Preset Description
default Inter + JetBrains Mono, zinc neutrals, blue accent
gravv Bricolage Grotesque display + Geist Mono body, green accent, dark code surfaces in both modes

A preset defines the full token set — colours for light and dark, the named text styles, radii and elevation. themeOverrides is merged over the top, and the legacy colors block is applied last so existing sites are never changed by introducing a preset. See Precedence.

An unknown preset name falls back to default with a build-log warning rather than failing the build.

Brand Colors

Set your primary brand color in syntext.json:

{
  "colors": {
    "primary": "#6366F1"
  }
}

The primary color is used for:

  • Links
  • Buttons
  • Focus rings
  • Active navigation items
  • Code syntax accents

Additional Colors

{
  "colors": {
    "primary": "#6366F1",
    "accent": "#2AC3DE",
    "background": "#0F172A"
  }
}
Property Description
primary Main brand color
accent Secondary accent for highlights
background Page background (dark mode)

Dark Mode

Syntext is dark-first by design. Users can toggle between modes, and their preference is saved.

Configure default mode:

{
  "theme": {
    "defaultMode": "dark"
  }
}

Options: "dark", "light", "system" (follows OS preference).

Typography

Font roles

A theme sets three font roles. Every named text style refers to one of them, so changing a role restyles everything that uses it.

Role Used by default preset
display H1–H4 Inter
body Body copy, navigation, labels, captions Inter
mono Code blocks, inline code JetBrains Mono

A theme is free to make body monospace — the gravv preset sets body, nav and labels in Geist Mono, with only headings in a display face.

Custom fonts

{
  "themeOverrides": {
    "fonts": {
      "display": "Plus Jakarta Sans",
      "body": "Nunito Sans",
      "mono": "Fira Code"
    }
  }
}

Families are fetched from Google Fonts automatically. When one family serves two roles, it appears once in the request with the weights unioned.

For more control, pass an object:

{
  "themeOverrides": {
    "fonts": {
      "display": {
        "family": "Bricolage Grotesque",
        "weights": [500, 600],
        "fallback": "Inter, system-ui, sans-serif"
      }
    }
  }
}

Load only the weights you use — each extra weight is bytes on the wire. If a named style needs a weight the family does not load, the browser synthesizes it and the result looks subtly wrong.

To self-host a face, set webfont: "self-hosted" so no Google Fonts request is made, and supply the @font-face rule yourself via customCSS.

Named text styles

Each style carries family role, weight, size, line-height and letter-spacing. All values are px.

{
  "themeOverrides": {
    "typography": {
      "display-h1": { "size": 34, "lineHeight": 42, "letterSpacing": -1.5, "weight": 600 },
      "nav-section": { "size": 11, "letterSpacing": 0.77 }
    }
  }
}

Letter-spacing from Figma needs converting. Figma lets a text style express letter-spacing in either pixels or percent, but its API reports the bare number with the unit stripped — a style set to 7% and a style set to 7px both come across as 7.

Copied straight into letterSpacing, a percent value lands as pixels. On small text the result is dramatic: 7 on an 11px label is 64% tracking, roughly double the intended width, and it looks like a rendering fault rather than a mis-transcribed number.

Check the unit in Figma's type panel before copying. Where it reads percent, convert it: letterSpacing = size x percent / 100. An 11px label at 7% is 0.77.

Available styles: display-h1, heading-h2, heading-h3, heading-h4, body-lg, body-md, body-sm, label-md, label-sm, strong-sm, strong-md, code-md, code-sm, caption, eyebrow, nav-item, nav-section, mono-label.

The API-reference scale

API reference pages carry more structure per screen than a guide, so they can run a denser type scale. Set it with apiTypography, which takes the same style names and applies scoped to that surface only — guide pages keep the base scale.

{
  "themeOverrides": {
    "apiTypography": {
      "display-h1": { "size": 32, "lineHeight": 38 },
      "body-md": { "size": 15, "lineHeight": 27 }
    }
  }
}

A partial patch inherits everything it does not restate from the base scale, so overriding just size keeps the family, weight and tracking. Sites that want one scale everywhere simply omit the block.

Add your logo for light and dark modes:

{
  "logo": {
    "light": "/logo-light.svg",
    "dark": "/logo-dark.svg"
  }
}

Place logo files in your public/ directory.

For a single logo that works on both backgrounds:

{
  "logo": "/logo.svg"
}

Favicon

{
  "favicon": "/favicon.ico"
}

Or use multiple sizes:

{
  "favicon": {
    "default": "/favicon.ico",
    "apple": "/apple-touch-icon.png",
    "32": "/favicon-32x32.png",
    "16": "/favicon-16x16.png"
  }
}

Code Theme

Syntax colours come from the theme's syntax-* tokens:

{
  "themeOverrides": {
    "colors": {
      "light": {
        "syntax-keyword": "#c792ea",
        "syntax-string": "#9ece6a",
        "syntax-key": "#7dd3c0",
        "syntax-number": "#ffb86c",
        "syntax-punct": "#8a93a6",
        "syntax-comment": "#5f6a7d",
        "syntax-text": "#cbd5e1"
      },
      "dark": { "syntax-keyword": "#c792ea" }
    }
  }
}

bg-code and bg-code-header set the code surface itself, and content-code sets inline code. A theme may keep code surfaces dark in both modes — the gravv preset does.

The codeTheme key is accepted but has no effect. The highlighter bakes colours into the HTML at build time, so they cannot be retargeted with CSS afterwards — which is why the palette has to come from the theme tokens, before the page is generated.

Custom CSS

Inject custom CSS for advanced styling:

{
  "customCSS": "/custom.css"
}

Create public/custom.css:

/* Override link color */
:root {
  --color-primary: #10B981;
}

/* Custom heading style */
h1 {
  letter-spacing: -0.02em;
}

/* Style code blocks */
pre {
  border-radius: 12px;
}

Available CSS Variables

Every token is emitted as a CSS custom property, so customCSS can reference the resolved theme instead of hardcoding values. Colours are emitted twice — once on :root for light mode and once under [data-theme="dark"].

/* Colours — content */
--color-content-primary   --color-content-secondary  --color-content-muted
--color-content-inverse   --color-content-code

/* Colours — surfaces */
--color-bg-page     --color-bg-sidebar  --color-bg-subtle  --color-bg-raised
--color-bg-header   --color-bg-code     --color-bg-code-header

/* Colours — lines */
--color-border  --color-border-subtle  --color-border-code

/* Colours — accent */
--color-accent  --color-accent-hover  --color-accent-subtle
--color-accent-strong  --color-accent-muted  --color-on-accent

/* Colours — status */
--color-status-success  --color-status-success-bg
--color-status-warning  --color-status-warning-bg  --color-status-error

/* Colours — call to action and hero card (inverted surfaces, so they cannot be
   derived from the page background) */
--color-cta-bg        --color-cta-fg
--color-hero-card-bg  --color-hero-card-fg

/* Colours — API parameter fields */
--color-param-name  --color-param-required  --color-param-required-bg

/* Colours — HTTP methods (foreground + badge background) */
--color-method-get   --color-method-get-bg    --color-method-post
--color-method-post-bg  --color-method-put    --color-method-put-bg
--color-method-patch --color-method-patch-bg  --color-method-delete
--color-method-delete-bg  --color-method-neutral  --color-method-neutral-bg

/* Colours — syntax (also drives the highlighter) */
--color-syntax-text     --color-syntax-keyword  --color-syntax-string
--color-syntax-key      --color-syntax-number   --color-syntax-punct
--color-syntax-comment

/* Fonts */
--font-display  --font-body  --font-mono

/* Type styles: a CSS `font:` shorthand, plus tracking (which the
   shorthand cannot express) */
--type-display-h1   --tracking-display-h1
--type-heading-h2   --tracking-heading-h2
--type-body-md      --tracking-body-md
/* …one pair per named text style */

/* Radii and elevation */
--radius-sm  --radius-md  --radius-lg  --radius-xl  --radius-full
--shadow-sm  --shadow-md  --shadow-lg

Use them like any custom property:

.my-callout {
  background: var(--color-accent-subtle);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font: var(--type-body-md);
  letter-spacing: var(--tracking-body-md);
}

Older variable names (--primary, --bg, --text, --border, --font-sans) are still emitted as aliases onto the tokens above, so existing customCSS keeps working. Prefer the --color-* names for anything new.

Custom JavaScript

Add custom scripts (analytics, chat widgets, etc.):

{
  "scripts": [
    {
      "src": "https://analytics.example.com/script.js",
      "async": true
    },
    {
      "src": "/custom.js",
      "defer": true
    }
  ]
}

Top Banner

Display announcements or promotional banners:

{
  "banner": {
    "text": "📢 Version 2.0 is here!",
    "link": "/changelog/v2",
    "dismissible": true
  }
}

Every page ends with a full-width footer band. By default it shows your social links and a "Powered by Syntext" credit. Add footer.columns for a rich footer with your logo and grouped link columns:

{
  "footer": {
    "columns": [
      {
        "title": "Product",
        "links": [
          { "label": "Documentation", "url": "/guides/quickstart" },
          { "label": "Status", "url": "https://status.example.com" }
        ]
      },
      {
        "title": "Legal",
        "links": [
          { "label": "Terms", "url": "https://example.com/terms" },
          { "label": "Privacy", "url": "https://example.com/privacy" }
        ]
      }
    ]
  }
}

Relative URLs stay on your doc site; absolute URLs open in a new tab. See the footer reference for details.

Add social media links to the header:

{
  "socialLinks": [
    { "platform": "GitHub", "url": "https://github.com/yourorg" },
    { "platform": "X", "url": "https://x.com/yourhandle" },
    { "platform": "Discord", "url": "https://discord.gg/your-server" }
  ]
}

Supported platforms: GitHub, X, Discord, LinkedIn, YouTube, Slack.

Was this page helpful?