So you want to create a theme for VoidReader? Excellent. Just drop a JSON file in the themes folder and you're done. No coding, no compiling, no PRs.
This guide walks you through everything - from the quick start to the full color system.
- Open the themes folder: View menu → "Open Themes Folder..."
- Find the example: VoidReader creates
example-theme.jsonfor you - Copy and edit: Duplicate it, rename it, change the colors
- Reload: View menu → "Reload Themes" (or restart the app)
- Select: Settings → Appearance → pick your theme
That's it. Your theme appears in the picker alongside System and Catppuccin.
VoidReader themes follow a few guiding principles:
-
Light AND Dark Required - Every theme must provide both a light and dark variant. No single-mode themes. Users expect their apps to respect system appearance, and we honor that.
-
System Theme is Special - The default "System" theme uses native macOS semantic colors (
NSColor.textColor,NSColor.linkColor, etc.). It's designed to blend seamlessly into macOS. Custom themes like Catppuccin override these with explicit colors. -
Syntax Colors, Not Chrome - Themes control text/syntax colors, not window chrome, toolbar styling, or layout. VoidReader uses native macOS controls that inherit system appearance.
-
Semantic Color Slots - You define colors by purpose (headings, links, code), not by location. The app maps these semantically across all surfaces.
Think of a VoidReader theme as a color palette with 12 slots. You provide two palettes - one for light mode, one for dark mode. That's it. No CSS, no layout rules, just colors.
┌─────────────────────────────────────────────────────────┐
│ AppTheme │
│ ├── id: "my-theme" │
│ ├── displayName: "My Awesome Theme" │
│ ├── lightPalette: ThemePalette (12 colors) │
│ └── darkPalette: ThemePalette (12 colors) │
└─────────────────────────────────────────────────────────┘
Each ThemePalette has exactly 12 color slots:
| Slot | Purpose | Used For |
|---|---|---|
base |
Primary background | Reader view background (non-System themes) |
text |
Primary text | Body text, paragraphs |
subtext0 |
Muted text | Secondary labels, markers, emphasis delimiters |
| Slot | Purpose | Used For |
|---|---|---|
surface0 |
Elevated surface | Code block backgrounds, cards |
surface1 |
Borders/dividers | Table borders, separators |
These are your syntax highlighting colors:
| Slot | Purpose | Editor Syntax | Reader View* |
|---|---|---|---|
mauve |
Headings | # Heading markers + text |
Heading text |
blue |
Links | [text](url) |
Link text |
green |
Code/Math | `inline` and ``` blocks |
Inline math |
teal |
Lists | - and 1. markers |
List markers |
lavender |
Quotes | > markers |
Blockquote text |
red |
Errors | (reserved for future) | - |
yellow |
Warnings | (reserved for future) | - |
*Reader View colors only apply when "Apply Theme to Reader" is enabled in Settings.
Your theme colors flow to these surfaces:
The syntax-highlighted markdown source. Uses AST-based highlighting:
- Headings →
mauve - Links →
blue - Code spans/blocks →
green - List markers →
teal - Blockquote markers →
lavender - Emphasis markers (
**,*,~~) →subtext0 - Body text →
text
The rendered markdown. When "Apply Theme to Reader" is enabled (Settings → Appearance):
- Body text →
text - Headings →
mauve - Links →
blue - List markers →
teal - Blockquotes →
lavender - Inline math →
green - Code backgrounds →
surface0(with opacity) - Secondary text (HR) →
subtext0
Note: By default, reader view uses native macOS semantic colors. Enable "Apply Theme to Reader" to use your theme's palette.
Diagrams get themeVariables generated from your palette:
- Node backgrounds →
surface0 - Node text →
text - Lines/arrows →
subtext0 - Borders →
surface1
Currently uses Highlightr's built-in themes (atom-one-dark/light). Full theme integration is a future enhancement.
Go to View → Open Themes Folder...
This opens:
~/Library/Application Support/VoidReader/themes/
VoidReader automatically creates an example-theme.json to get you started.
Copy the example or create a new .json file:
{
"id": "my-theme",
"displayName": "My Awesome Theme",
"light": {
"base": "#ffffff",
"text": "#24292e",
"subtext0": "#6a737d",
"surface0": "#f6f8fa",
"surface1": "#e1e4e8",
"mauve": "#6f42c1",
"blue": "#0366d6",
"green": "#22863a",
"teal": "#0598bc",
"lavender": "#6f42c1",
"red": "#d73a49",
"yellow": "#dbab09"
},
"dark": {
"base": "#0d1117",
"text": "#c9d1d9",
"subtext0": "#8b949e",
"surface0": "#161b22",
"surface1": "#30363d",
"mauve": "#d2a8ff",
"blue": "#58a6ff",
"green": "#7ee787",
"teal": "#39c5cf",
"lavender": "#d2a8ff",
"red": "#f85149",
"yellow": "#d29922"
}
}Requirements:
idmust be unique (lowercase, no spaces recommended)displayNameis what appears in the Settings picker- Both
lightanddarkpalettes are required - All 12 color slots must be present in each palette
- Colors must be valid hex (
#RGBor#RRGGBB)
Either:
- View → Reload Themes (Cmd+Shift+Option+T)
- Or restart VoidReader
Settings → Appearance and pick your theme from the dropdown.
That's it! No compiling, no code, no PRs.
These are fixed by design:
| Aspect | Why |
|---|---|
| Typography | Font family, sizes, heading scales are user preferences, not theme concerns |
| Layout | Margins, padding, spacing are fixed for consistent reading experience |
| Color slot names | The 12 slots are semantic - can't add or rename them |
| Light + Dark requirement | Every theme must have both variants |
| Window chrome | Title bar, toolbar use native macOS styling |
| System theme behavior | The System theme always uses macOS semantic colors |
- Ensure sufficient contrast between
textandbase subtext0should be readable but clearly muted- Test both light and dark modes!
- Pick accent colors that work together
mauve,blue,green,teal,lavenderwill appear near each other in complex documents- Consider colorblind users - don't rely solely on red/green distinction
Look at established color schemes for inspiration:
- Catppuccin - What we ship
- Dracula
- Nord
- Solarized
- GitHub's Primer
Don't just test with "Hello World". Open a complex markdown file with:
- Multiple heading levels
- Nested lists
- Code blocks with different languages
- Mermaid diagrams
- Tables
- Blockquotes
- Inline code mixed with links
Some things we'd like to add:
- Full code block theming - Custom Highlightr CSS from theme palette
- Theme preview - Live preview swatches in Settings
- File watcher - Auto-reload when theme files change
- Theme validation UI - Show errors for malformed themes in Settings
- Check the file is in
~/Library/Application Support/VoidReader/themes/ - Ensure the file extension is
.json - Use View → Reload Themes or restart the app
- Check Console.app for error messages (search "VoidReader")
- Colors must be
#RGBor#RRGGBBformat - The
#is optional but recommended - No alpha channel support (no
#RRGGBBAA)
- Verify you're testing in the right appearance mode (light/dark)
- Check that the color is in the correct palette (
lightvsdark) - Remember: some colors only affect editor mode, not reader mode
- Use a JSON validator (many free online)
- Common issues: trailing commas, missing quotes, unescaped characters
- The example theme is valid - diff against it to find issues
~/Library/Application Support/VoidReader/themes/
See Sources/VoidReaderCore/Theming/CatppuccinTheme.swift for the Catppuccin implementation. It's a well-documented color scheme with official hex values.
- View → Open Themes Folder... - Opens themes directory in Finder
- View → Reload Themes (Cmd+Shift+Option+T) - Reloads all user themes
Questions? Issues? Open a GitHub issue.