Skip to content

Customize

Themes

Install a ready-made look with one command. A theme is one JSON file, so there is nothing else to wire up.

A theme is one JSON file. It carries colors, spacing, the shape of the progress counter, the arrow, the spotlight and the scrim — everything about how a tour looks, and nothing about what it does.

Because it is only data, installing one writes one file into your project. There is no package to add at runtime, no build step, and nothing to keep in sync with your framework.

  1. Add a theme. Pick one from the gallery below:

    Terminal window
    npx @docentjs/cli theme add ledger

    This writes docent-theme.json, after checking it. Use --out to put it somewhere else, and docent theme list to see every theme.

  2. Hand it to the renderer. One line, wherever you already set up Docent:

    import { createTour } from '@docentjs/dom'
    import theme from './docent-theme.json'
    createTour(tour, { renderer: { template: theme } })
  3. That is it. Every tour in the app now uses the theme.

A theme is just a URL away, so anyone can publish one. The CLI checks it before writing anything, and refuses a file that is not a valid theme:

Terminal window
npx @docentjs/cli theme add https://example.com/my-theme.json

The same check is available in code as validateTheme(theme) from @docentjs/dom/validate, for themes you load at runtime.

Prefer not to use the CLI? Copy the JSON from any card below into your project instead. It is the same file.

Open the file and change it. Every field is documented in theming and arrows and spotlight; the ones a theme uses most:

Field What it does
theme colors, radius, width, padding, font, shadow, scrim
eyebrow a small line above every title. {tour} becomes the tour’s name
progress meter, count, ticks, dots or none
arrow caret, none, or one of the ten drawn connectors
spotlight padding, radius, shape and ring around the target
overlay dim, blur, vignette or none
mobile on phones: layout (auto, float, dock) and card (stories, compact, classic); a tour’s own mobile wins
css anything the fields above do not cover, scoped to the popover

A tour can override any of it, so one tour can go its own way without a second theme:

{ "options": { "template": "ledger", "progress": "dots", "eyebrow": "New in 2.4" } }

Press Preview to run a real tour with that theme. These are live — the card is the library rendering, not a picture of it.

Bloomdots progress · squiggle arrow
npx @docentjs/cli theme add bloom
bloom.json
{
  "name": "Bloom",
  "theme": {
    "background": "oklch(99% 0.008 330)",
    "foreground": "oklch(26% 0.05 330)",
    "muted": "oklch(58% 0.04 330)",
    "accent": "oklch(58% 0.21 350)",
    "accentForeground": "oklch(99% 0.008 350)",
    "connector": "oklch(58% 0.21 350)",
    "ring": "oklch(78% 0.16 350)",
    "radius": 20,
    "width": 336,
    "padding": "20px 22px 16px",
    "shadow": "0 2px 4px oklch(40% 0.12 330 / 0.1), 0 28px 60px -28px oklch(40% 0.14 330 / 0.6)",
    "duration": 260
  },
  "progress": "dots",
  "arrow": "squiggle",
  "spotlight": {
    "padding": 10,
    "radius": 16,
    "ring": "pulse",
    "animate": true
  },
  "overlay": {
    "color": "oklch(30% 0.09 330)",
    "opacity": 0.38
  },
  "css": ".title { font-size: 19px; letter-spacing: -0.015em; } .button { border-radius: 999px; padding-inline: 14px; }"
}
Broadsheetcount progress · eyebrow
npx @docentjs/cli theme add broadsheet
broadsheet.json
{
  "name": "Broadsheet",
  "theme": {
    "background": "oklch(98.6% 0.006 85)",
    "foreground": "oklch(21% 0.012 60)",
    "muted": "oklch(52% 0.014 60)",
    "accent": "oklch(45% 0.14 28)",
    "accentForeground": "oklch(98% 0.008 85)",
    "connector": "oklch(45% 0.14 28)",
    "radius": 3,
    "width": 360,
    "padding": "18px 20px 16px",
    "duration": 190
  },
  "eyebrow": "{tour}",
  "progress": "count",
  "spotlight": {
    "padding": 8,
    "radius": 3,
    "ring": "hairline"
  },
  "overlay": {
    "color": "oklch(24% 0.03 60)",
    "opacity": 0.46
  },
  "css": ".title { font-family: ui-serif, Georgia, 'Times New Roman', serif; font-size: 21px; letter-spacing: -0.01em; } .eyebrow { letter-spacing: 0.14em; } .footer { margin-top: 20px; padding-top: 12px; border-top: 1px solid color-mix(in oklch, var(--docent-fg) 14%, transparent); }"
}
Carbonticks progress
npx @docentjs/cli theme add carbon
carbon.json
{
  "name": "Carbon",
  "theme": {
    "background": "oklch(13% 0.01 285)",
    "foreground": "oklch(99% 0.002 285)",
    "muted": "oklch(88% 0.008 285)",
    "accent": "oklch(90% 0.18 100)",
    "accentForeground": "oklch(13% 0.01 285)",
    "connector": "oklch(90% 0.18 100)",
    "ring": "oklch(90% 0.18 100)",
    "radius": 4,
    "width": 340,
    "padding": "18px",
    "shadow": "none"
  },
  "progress": "ticks",
  "spotlight": {
    "padding": 6,
    "radius": 2,
    "ring": "solid"
  },
  "overlay": {
    "opacity": 0.8
  },
  "css": ".popover { box-shadow: 0 0 0 2px var(--docent-accent); } .marks i { height: 3px; }"
}
Consolecount progress · eyebrow
npx @docentjs/cli theme add console
console.json
{
  "name": "Console",
  "theme": {
    "background": "oklch(24.5% 0.016 60)",
    "foreground": "oklch(94% 0.008 60)",
    "muted": "oklch(72% 0.012 60)",
    "accent": "oklch(74% 0.165 58)",
    "accentForeground": "oklch(19% 0.04 60)",
    "connector": "oklch(74% 0.165 58)",
    "ring": "oklch(74% 0.165 58)",
    "radius": 8,
    "width": 344,
    "padding": "16px 16px 12px",
    "shadow": "0 24px 56px -24px oklch(8% 0.02 60 / 0.95)",
    "duration": 200
  },
  "eyebrow": "{tour}",
  "progress": "count",
  "count": "{current2}/{total2}",
  "spotlight": {
    "ring": "hairline",
    "padding": 8,
    "radius": 6,
    "animate": true
  },
  "overlay": {
    "color": "oklch(12% 0.014 60)",
    "opacity": 0.66
  },
  "css": ".count { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.02em; } .button { border-radius: 6px; }"
}
Ledgerticks progress · eyebrow
npx @docentjs/cli theme add ledger
ledger.json
{
  "name": "Ledger",
  "theme": {
    "background": "oklch(99.3% 0.005 95)",
    "foreground": "oklch(24% 0.014 95)",
    "muted": "oklch(58% 0.011 95)",
    "accent": "oklch(42% 0.082 155)",
    "accentForeground": "oklch(98% 0.012 155)",
    "connector": "oklch(42% 0.082 155)",
    "ring": "oklch(98% 0.01 95)",
    "radius": 12,
    "width": 352,
    "padding": "18px 18px 14px",
    "shadow": "0 1px 2px oklch(24% 0.014 95 / 0.08), 0 26px 52px -26px oklch(24% 0.014 95 / 0.55)",
    "duration": 220
  },
  "eyebrow": "{tour}",
  "progress": "ticks",
  "count": "{current2} / {total2}",
  "spotlight": {
    "padding": 10,
    "radius": 10,
    "animate": true
  },
  "overlay": {
    "color": "oklch(32% 0.03 95)",
    "opacity": 0.44
  }
}
Nocturnedots progress · eyebrow · curve arrow
npx @docentjs/cli theme add nocturne
nocturne.json
{
  "name": "Nocturne",
  "theme": {
    "background": "oklch(23% 0.013 265)",
    "foreground": "oklch(95% 0.004 265)",
    "muted": "oklch(74% 0.008 265)",
    "accent": "oklch(95% 0.004 265)",
    "accentForeground": "oklch(14.5% 0.008 265)",
    "connector": "oklch(68% 0.13 48)",
    "ring": "oklch(68% 0.13 48)",
    "radius": 10,
    "width": 340,
    "padding": "16px 16px 12px",
    "shadow": "0 26px 60px -26px oklch(6% 0.01 265 / 0.95)",
    "duration": 200
  },
  "eyebrow": "{tour}",
  "progress": "dots",
  "arrow": "curve",
  "spotlight": {
    "padding": 8,
    "radius": 9,
    "ring": "glow",
    "animate": true
  },
  "overlay": {
    "color": "oklch(10% 0.01 265)",
    "opacity": 0.68
  }
}
Quietno progress · curve arrow · no scrim
npx @docentjs/cli theme add quiet
quiet.json
{
  "name": "Quiet",
  "theme": {
    "background": "oklch(99.4% 0.003 285)",
    "foreground": "oklch(23% 0.018 285)",
    "muted": "oklch(56% 0.014 285)",
    "accent": "oklch(26% 0.02 285)",
    "radius": 8,
    "width": 300,
    "padding": "14px 16px 12px",
    "shadow": "0 1px 2px oklch(23% 0.02 285 / 0.08), 0 6px 16px -6px oklch(23% 0.02 285 / 0.14)"
  },
  "progress": "none",
  "arrow": "curve",
  "spotlight": {
    "ring": "glow",
    "padding": 6,
    "radius": 8
  },
  "overlay": {
    "style": "none"
  },
  "css": ".title { font-size: 16px; }"
}

Start from the closest theme, change the tokens, and keep going until the fields run out. If you reach something no field covers, css is the next step, and slots after that — but a theme that needs code is no longer something you can publish, share or let a tool edit, so it is worth staying in JSON as long as you can.