Personalization
Personalization keeps the bundled Folio template and changes the theme data it receives. This is the right level when the docs should still feel like Folio, but with your project’s brand, typography, accent, spacing, and header defaults.
Basic Theme Options
theme:
preset: "pastel"
dark_mode: true
logo: "docs/assets/logo.svg"
favicon: "docs/assets/favicon.ico"| Field | Purpose |
|---|---|
preset | Default visual preset for the generated site; omitted, Folio Pastel with Ink. |
dark_mode | true (the default) offers light, dark, and system modes through next-themes; false keeps the site light, unless the reader applies a dark-only palette such as a dark Omarchy one. |
logo | Copies a project logo into the generated site and shows it beside the project name in the docs and landing navbars. |
favicon | Copies a project favicon into the generated site. |
preset must be a preset the theme picker shows: a
built-in id, one a
theme package declares, or a new id
that theme.name, theme.tokens or theme.style defines (see
Project Presets). Any other value stops the build with the
list of valid ids and the nearest one, so preset: "beakon" asks whether you
meant beacon.
Dark mode is enabled by default. When enabled, readers can use the mode radios in
the theme picker or press d outside form fields to switch between light and dark
modes. theme.header.theme_toggle: true adds a one-click light/dark toggle to the
landing and the docs navbars alike; without it neither navbar has one. With
dark_mode: false the site stays light whatever the reader’s system or saved
preference: the picker shows no mode control, no toggle is rendered, and d does
nothing. theme.header.theme_toggle: true is then ignored with a warning. A
dark-only palette the reader applies still turns the page dark while it is
applied.
The logo path is resolved from the project directory and must exist; a missing
file stops the build. The image takes the monogram’s place in the docs navbar
and the landing navbar, and is served under deploy.base_path when one is set.
The previews page keeps the monogram.
Theme Picker
Every generated site includes the theme picker on the landing page, the docs and
the previews. The palette button in the navbar, or the t key, opens it: every
theme as a slide drawn in its own colors, the front theme’s colors as swatches
under it, and light, dark and system mode in its header unless
theme.dark_mode is false. The carousel walks six Folio styles, five Folio Pastel palettes and
four Omarchy palettes. Arrows, swipes and swatches apply and save each choice
immediately. Done or Escape closes the picker and keeps the selected theme.
Customize, or the c key, opens the picker’s second step
for the front theme: its own options, typography, reading rhythm, corners,
borders, code block frame, page frame and background, text and accent colors.
Each change updates the page and saves immediately.
Reader preferences are persisted in a project-scoped localStorage key derived
from the configured default preset. When that default changes from Roller to
Folio Pastel, existing reader choices are reused unless the new key already
exists. Folio also renders the default theme CSS into
the page before hydration so the site does not flash through unconfigured
typography or colors.
See Theme picker for the picker’s keys, the built-in preset catalog and the TypeScript preset contract.
Tune Defaults
Use theme.tune to set the defaults of the picker’s Customize step without
creating a new preset. They are defaults only and restrict nothing:
theme:
preset: "beacon"
tune:
font: "geist"
accent: "laurel"
surface: "paper"
width: "wide"
rhythm: "compact"
borders: "fine"
code: "terminal"
radius: "0.3rem"Common aliases map to the internal configurator ids:
| YAML key | Internal control |
|---|---|
font | fontId |
accent or color | colorId |
surface | surfaceColorId |
shell | shellPaddingId |
width or content_width | contentWidthId |
rhythm or reading | rhythmId |
borders or border | borderId |
code or code_blocks | codeTreatmentId |
Each value must be one the configurator offers for that control:
| Control | Values |
|---|---|
fontId | folio, sans, geist, serif, mono, terminal, grotesque |
colorId | ink, laurel, indigo, copper |
surfaceColorId | preset, paper, moss, mist |
shellPaddingId | preset, flush, frame, gallery |
contentWidthId | preset, focus, docs, wide |
rhythmId | preset, compact, balanced, roomy |
borderId | preset, fine, structured, ruled |
codeTreatmentId | preset, soft, framed, plate, terminal |
Any other value stops the build with the list and the nearest value, for example
theme.tune.rhythm must be one of 'preset', 'compact', 'balanced', 'roomy'; got 'dense'. A key that is none of the above warns, names the key it most likely
meant, and is ignored.
font: "geist" selects the bundled Geist and Geist Mono font pair and maps the
public --font-sans and --font-mono tokens used by Tailwind utilities.
theme.radius (and its theme.tune.radius alias) is validated against the
fixed radius scale: "0", "0.3rem", "0.5rem", "0.75rem", or "1rem".
The named aliases "none", "sm", "md", "lg", and "full" map onto the
same scale (matching the picker’s Corners labels). Any other value fails
config validation, because the configurator maps the configured radius onto
this fixed scale.
Project Presets
When a project supplies name, description, scene, preview, style,
tokens, or variants, Folio emits theme/project-theme.ts during template preparation and
adds the project preset before the built-in preset library.
docs.yaml
A project-owned theme preset generated from safe YAML data.
theme:
preset: "acme"
name: "Acme"
description: "Operational docs theme"
preview:
light: "oklch(0.64 0.12 155)"
dark: "oklch(0.78 0.14 155)"
tune:
font: "geist"
width: "wide"
rhythm: "compact"
radius: "0.3rem"
header:
brand: "Acme"
badge: "Docs"
repo: "https://github.com/acme/sdk"
search: true
theme_toggle: true
action_label: "Dashboard"
action_href: "https://app.acme.dev"
tokens:
light:
--background: "oklch(0.985 0.01 150)"
--foreground: "oklch(0.13 0.02 150)"
--primary: "oklch(0.47 0.14 155)"
--ring: "oklch(0.47 0.14 155)"
dark:
--background: "oklch(0.10 0.01 150)"
--foreground: "oklch(0.94 0.01 150)"
--primary: "oklch(0.73 0.15 155)"
--ring: "oklch(0.73 0.15 155)"
style:
--folio-content-max-width: "82rem"
--folio-section-gap: "2.75rem"
--folio-card-padding: "1.1rem"
--folio-code-bg: "oklch(0.14 0.01 150)"- theme.name
- Label shown in the theme picker's Project group.
string - theme.preview
- Light and dark swatches for the preset preview.
object - theme.style
- Layout and typography CSS custom properties.
object - theme.tokens
- Light and dark shadcn/project CSS variable overrides.
object - theme.header
- Docs header brand, badge, repo, search, and action controls.
object - theme.variants
- Project-owned preset controls with options, swatches, and token overrides.
object
tokens.light and tokens.dark accept CSS custom properties such as shadcn
tokens (--background, --card, --border, --chart-1) and project tokens
(--brand-accent). style accepts layout variables such as
--folio-content-max-width, --folio-section-gap, --folio-card-padding,
--folio-code-bg, --folio-workspace-shell-topbar,
--folio-workspace-shell-topbar-blur, and
--folio-workspace-shell-topbar-border. The un-prefixed legacy spellings
(for example --content-max-width) are still accepted for compatibility but
are deprecated; new configs should use the canonical --folio-* names.
Header URL Validation
The header can carry links: theme.header.repo and theme.header.action_href.
Folio validates these URLs at config-load time against an allowlist of safe
schemes. Permitted values are:
httpandhttpsURLs (for examplehttps://github.com/acme/sdk);mailto:links; and- relative URLs (for example
/dashboardordocs/index).
Unsafe schemes — notably javascript: and data: — are rejected. A header
link using a rejected scheme raises a config error and fails the build, naming
the offending field, rather than being emitted into the generated site. This
keeps script-injection payloads out of the docs header even when the
docs.yaml comes from an untrusted source. Use an http(s), mailto:, or
relative URL for any header link.
The top-level project.repo is validated less strictly because repository
URLs legitimately use non-web schemes: ssh://, git://, git+https://, and
scp-style git@host:path forms are accepted, and only schemes that could
execute script or read local files (javascript:, data:, vbscript:,
file:) are rejected.
Variants
Variants let a project expose its own theme controls. Each option can set
a swatch, preview colors, style overrides, and light/dark token overrides while
inheriting the base project preset. The first control whose options set no
style only recolors the preset, and the picker shows it as the Colours row
under the preset’s slide; every other control is a row of its Customize step.
theme:
preset: "acme"
name: "Acme"
variants:
color:
label: "Color"
default: "default"
options:
default:
label: "Default"
swatch: "oklch(0.47 0.14 155)"
ocean:
label: "Ocean"
swatch: "oklch(0.56 0.16 230)"
tokens:
light:
--primary: "oklch(0.50 0.16 230)"
--ring: "oklch(0.50 0.16 230)"
dark:
--primary: "oklch(0.74 0.15 230)"
--ring: "oklch(0.74 0.15 230)"If an option has swatch but no full light/dark preview, Folio uses the
swatch for both preview modes.
Every option combination across all variant controls is resolved and embedded
into each generated page, so theme.variants is capped at 256 combinations
(the product of each control’s option count). Exceeding the cap fails config
validation; reduce the number of controls or options.
CSS Variables
Folio themes use oklch colors through CSS custom properties:
:root {
--background: oklch(0.995 0 0);
--foreground: oklch(0.145 0.005 285);
--primary: oklch(0.51 0.14 170);
--primary-foreground: oklch(0.99 0 0);
--muted: oklch(0.96 0.003 264);
--muted-foreground: oklch(0.50 0.015 264);
--card: oklch(0.995 0 0);
--card-foreground: oklch(0.145 0.005 285);
--border: oklch(0.91 0.004 264);
--input: oklch(0.91 0.004 264);
--ring: oklch(0.51 0.14 170);
--radius: 0.5rem;
}
.dark {
--background: oklch(0.14 0.004 285);
--foreground: oklch(0.92 0.004 264);
--primary: oklch(0.72 0.17 170);
--border: oklch(1 0 0 / 8%);
}Sidebar-specific variables control the left navigation independently from the main content area:
| Variable | Purpose |
|---|---|
--sidebar | Sidebar background. |
--sidebar-foreground | Sidebar text color. |
--sidebar-primary | Sidebar active item color. |
--sidebar-accent | Sidebar hover/focus background. |
--sidebar-border | Sidebar border color. |
Motion variables time the mobile menu and the Term card on every preset, and the transitions the Pastel preset adds. The rest of the docs shell keeps its own fixed timings, Nextra’s and Folio’s:
| Variable | Default | Purpose |
|---|---|---|
--folio-motion-micro | 120ms | The close button answering a hover. |
--folio-motion-element | 200ms | The scrim fading in, and the close icon’s turn. |
--folio-motion-exit | 180ms | The scrim fading out. |
--folio-motion-max | 300ms | The menu sliding out, and Pastel’s light and dark reveal. |
--folio-motion-glide | 240ms | Pastel’s tab highlight gliding to the chosen tab, and the tab label’s colour. |
--folio-motion-fade | 160ms | Pastel’s newly chosen tab panel fading in. |
--folio-motion-drawer | 560ms | The menu springing in. |
--folio-ease-enter | cubic-bezier(0.2, 0.8, 0.2, 1) | The scrim fading in, the close button, and Pastel’s light and dark reveal. |
--folio-ease-exit | cubic-bezier(0.4, 0, 1, 1) | The menu and the scrim leaving. |
--folio-spring-drawer | a spring as linear() | The menu springing in; a cubic-bezier where linear() is not supported. |
Below Nextra’s md breakpoint (48rem, 768 px at the default font size) the
docs menu is a drawer, drawn from these variables:
| Variable | Default | Purpose |
|---|---|---|
--folio-drawer-inset | 12px | Gap between the drawer and the screen edges. |
--folio-drawer-width | 21.25rem | Drawer width, 340 px at the default font size and never wider than the screen minus both insets. |
--folio-drawer-radius | 24px | Corner radius. |
--folio-drawer-shadow | a hairline and a soft shadow | The drawer’s edge. |
--folio-drawer-scrim | --scrim at 30 % | The layer that dims the page behind the drawer. |
A theme that wants a full-screen menu sets --folio-drawer-inset: 0,
--folio-drawer-width: 100vw, --folio-drawer-radius: 0 and
--folio-drawer-shadow: none. Under reduced motion every transition and
animation on a docs page finishes at once; the landing and the roadmap keep
their own reduced-motion styles.
Callouts draw their six types from tone variables, one set per type: a fill
for the surface, an ink for the title and the text, and an accent for the icon
and the edge. <type> is note, info, tip, check, warning or
danger:
| Variable | Default | Purpose |
|---|---|---|
--folio-tone-<type>-accent | a hue mixed 75 % into --foreground: blue for info, violet for tip, green for check, --warning and --destructive for the last two; note takes --muted-foreground | The icon and the edge. Marker’s ok and danger labels take the check and danger accents. |
--folio-tone-<type>-fill | the accent at 12 % over --background; note mixes --muted-foreground at 8 % | The callout’s surface. |
--folio-tone-<type>-ink | the accent at 25 % into --foreground; note takes --foreground | The title and the text. |
A theme sets one by name, for example --folio-tone-tip-accent, and the fill
and ink mixed from it follow. The Pastel preset sets its own fills and inks
inside the callout, so a tone set at :root does not reach a Pastel callout.
| Variable | Default | Purpose |
|---|---|---|
--folio-surface-radius | var(--radius); Pastel sets 20px | The corner of a raised surface: the cards, panels, empty states, tab panels, file trees and callouts the Pastel preset draws. |
What Plugin Surfaces Rely On
The landing and the roadmap are drawn from the tokens on this page and nothing else: no colour of their own, no per-page override. That is the contract a plugin surface keeps, and it is what makes a preset switch or a theme package restyle every page at once.
| Family | Tokens | Set by |
|---|---|---|
| Surfaces and ink | --background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --secondary, --secondary-foreground, --border, --input, --ring, --radius | the preset; theme.tokens overrides |
| Emphasis | --primary, --primary-foreground, --destructive, --warning | the preset |
| Data | --chart-1 to --chart-5; the index pages, the preview cards and Mermaid draw from these | the preset |
| Type | --font-sans, --font-mono, --folio-heading-font-family, --folio-body-font-family | the preset; theme.tune |
| Layout | the --folio-* variables listed under Project Presets | the preset; theme.style |
Mermaid diagrams read the computed values of these tokens when they render and fall back to a neutral palette only when none resolve.
A plugin surface that needs a colour these families do not carry adds a named token in the same shape rather than a literal, so a theme can reach it.
When Personalization Is Not Enough
Use theme packages when the project needs to override files inside the bundled template while keeping Folio’s template as a base. Use custom templates when the project needs to own the entire frontend workspace.