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.
Logo
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
}
}
Footer
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.
Social Links
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.