Theming
Three contrast-tested presets, a brand accent, corner radius, and a light, dark or system default.
'theme' => [
'preset' => 'neutral',
'primary' => null,
'accent' => null,
'radius' => '0.5rem',
'default' => 'system',
],
Presets
| Preset | Notes |
|---|---|
neutral |
The default grey. primary (an oklch hue) only applies to this preset. |
ocean |
Cool blue page and a deep navy accent, with a near-black navy dark mode. |
laravel |
The laravel.com colours: warm sand neutrals and the Laravel red. |
An unknown preset name falls back to neutral.
Every preset defines the same set of tokens and is contrast-tested in light and dark mode. See Surfaces and Contrast.
Note
0.5 removed nine presets: black, vitepress, dusk, catppuccin, purple, solar,
emerald, ruby and aspen. A config that still names one falls back to neutral. Most of these presets only changed the
accent colour, which you can now set on any preset with accent.
Surfaces
Every preset uses the same three surface layers in both modes:
| Token | Used for | Position |
|---|---|---|
--background |
prose | the canvas, at the light or dark extreme |
--card |
sidebar, tab groups, the next-page card, popovers | one step toward mid-grey |
--muted |
code blocks, table headers, image mats, step markers | a second step |
Each layer steps away from the canvas in one direction: darker in light mode and lighter in dark mode. The reading surface stays the cleanest part of the page, and nested elements go one step deeper, so a code block inside a tab group sits below the group. The contrast test checks both the direction and the order.
Accent
accent recolours links, buttons and the focus ring on any preset. Set one colour for
both modes, or one per mode:
'accent' => '#7c3aed',
'accent' => [
'light' => '#1d4ed8',
'dark' => '#bfdbfe',
],
Hex, hsl() and oklch() values are accepted. The button label colour is derived from
the accent: a light accent gets dark text and a dark accent gets white text. A value that
cannot be parsed is ignored and the page renders with the preset's own colours.
primary is the older option and still works, but it only sets an oklch hue on
neutral. Use accent instead.
Contrast
Every shipped preset meets WCAG AA (4.5:1) in both modes for:
- body text on the page background
- secondary text (sidebar items, breadcrumbs, table of contents, page meta)
- link and accent text
- button labels on a filled button
Syntax highlighting uses one palette for light mode and one for dark, shared by every preset. Each token colour clears 4.5:1 on every preset's code surface, so a preset changes the code block background and leaves the token colours alone.
tests/View/PresetContrastTest.php reads the shipped stylesheets and fails if a palette
change drops below these ratios. A custom accent is not covered by that test, so choose
one that clears 4.5:1 against your page background.
Default mode
default sets the mode a first-time reader gets: light, dark or system, which
follows the operating system. Readers can change it from the theme menu. Their choice is
stored in localStorage and takes precedence over this setting from then on.
Radius
radius sets the --radius CSS variable on the document. The rounded corners of the
layout are derived from it, so cards, buttons, code blocks and the search dialog change
together. Only a few small inline details, such as inline code and task checkboxes, keep a
fixed radius. Any CSS length works.
Surfaces such as code blocks, tab groups, cards, tables and the search dialog are rounded
4px past radius. Anything set inside a surface, like the panel of a tab group, follows the
surface's curve: its radius is the surface's less the gap between them, so the two corners
nest evenly at any radius.
'radius' => '0.25rem',
Fonts
fonts is a top-level config key, separate from theme. Its value is added to the layout
head as raw HTML, so you can load a web font without publishing the layout:
'fonts' => '<link rel="stylesheet" href="https://example.com/inter.css">',
Loading the font does not apply it. Set the family in a stylesheet the layout already
loads, or add a <style> block to the same value:
'fonts' => '<link rel="stylesheet" href="https://example.com/inter.css">'
.'<style>body { font-family: Inter, sans-serif }</style>',
Where search sits
layout.search is either sidebar (the default, which also removes the top header) or
header.
With sidebar, the site name, the version switcher, search and the button that collapses the
sidebar are all in the sidebar. With header, they move to a bar across the top, and the
sidebar holds only the navigation.
Table of contents
The table of contents beside each page comes in two styles. Choose one with
layout.toc, or set VELLUM_LAYOUT_TOC:
'layout' => [
'toc' => 'window', // or 'line'
],
window is the default. One highlight sits behind every entry whose section is on screen,
so it shows how much of the page you are looking at. As you scroll, its edges hold on to
whole entries and spring to the next one when a section leaves the screen or a new one
comes onto it. Nested entries hang from a guide line.

line draws a line beside the entries and lights the part next to the sections on
screen, bending in and out with nested entries.

In both styles, a step from a steps block shows its number, filled while the step is on screen. In the line style the numbers sit on the line.