TopTech

Zettlr Markdown Notes Custom Theme Set Up And Its Amazing

Zettlr is one of the most underrated markdown note-taking apps out there, and with a custom Dracula dark theme it becomes genuinely beautiful. Here's exactly how I set mine up.

I've tried a lot of note-taking apps. Obsidian, Notion, Logseq, Typora, even just raw VS Code with a markdown extension. Each one has something going for it. But when I actually sat down and thought about what I want from a writing and research tool — offline-first, open source, actually good markdown rendering, no subscription — Zettlr kept coming back as the answer.

The one complaint I had for the longest time was the default appearance. The editor looked functional but not particularly inspiring, and the dark mode felt half-finished. Not ugly, just... beige in spirit. After spending some time digging into how Zettlr's theming system works, I built a custom dark theme on top of the Dracula palette and it completely changed how I feel about opening the app.

This is that setup guide.


What Is Zettlr and Why Should You Care

Zettlr is a free, open-source markdown editor built specifically for writers, researchers, and academics. It supports Zettelkasten-style linking between notes, has built-in Pandoc integration for exporting to PDF, Word, and LaTeX, and stores everything as plain .md files on your local disk.

That last part matters more than people realise. Your notes aren't in some proprietary database or synced to someone else's server by default. They're just files. You can move them, back them up with Git, open them in any text editor, and they'll still be readable in 20 years. That kind of longevity is why a lot of people who are serious about note-taking eventually drift toward plain markdown over apps like Notion.

What separates Zettlr from something like Obsidian is its focus on long-form writing and academic workflows. The citation manager integration, the project feature for multi-file documents, the built-in word count goals — these are things that matter when you're writing a 5,000-word essay or managing research notes, not just capturing quick thoughts.


How Zettlr's Custom CSS Works

Zettlr lets you inject custom CSS that applies on top of its default styles. This is different from creating a full theme — you're essentially overriding specific variables and classes rather than replacing the entire stylesheet.

To add your custom CSS:

  1. Open Zettlr
  2. Go to Files NavigationPreferencesPreferencessnippetsOpen snippets editor</> Custom Css.
  3. Paste your CSS into the editor
  4. Click Save

That's it. Zettlr applies it immediately — no restart needed, no build step. The custom CSS is scoped to the app's interface, which means you can target the editor surface, the sidebar, the toolbar, or the rendered preview separately.

The editor in Zettlr is built on CodeMirror 6, which means you're working with .cm-editor as the root element and CodeMirror's token classes (.tok-keyword, .tok-string, etc.) for syntax highlighting inside code blocks. The important thing to understand is that Zettlr exposes its own CSS custom properties (CSS variables prefixed with --zettlr-editor-*) for controlling editor colors, and these are the cleanest way to style it without fighting specificity battles.


The Dracula Theme — What I Used and Why

The Dracula color palette is one of the most consistent and well-designed dark color schemes in existence. It's not just a set of random hex codes — Dracula has an actual spec that defines which token types map to which colors, which makes it straightforward to implement in a new environment without guessing.

The palette I used:

Role Color Hex
Background Dark canvas #282a36
Foreground Body text #f8f8f2
Selection Highlighted text #44475a
Comment Dim text #6272a4
Cyan Types, tags, attributes #8be9fd
Green Functions #50fa7b
Orange Numbers, booleans #ffb86c
Pink Keywords #ff79c6
Purple Accents, headings #bd93f9
Red Errors #ff5555
Yellow Strings, emphasis #f1fa8c

The key design decision I made was to not assign colors to heading levels. A lot of custom Zettlr themes color each heading level a different accent — H1 is cyan, H2 is purple, and so on. That looks impressive in screenshots but is genuinely distracting when you're actually writing. Instead, I used size and weight to differentiate headings, and left the color as the default foreground. The Dracula accents appear in code blocks and inline syntax instead, where you actually want visual separation.


The Full Custom CSS

Here's the complete theme. Paste this into Preferences → Display → Custom CSS and save.

/* Fira Code as the editor font — applies in both light and dark mode */
.cm-editor {
  font-family: Fira Code !important;
}

/* ================================
   Editor surface + text
   ================================ */
body.dark .cm-editor {
  --zettlr-editor-scroller-bg: #282a36;
  --zettlr-editor-scroller-color: #f8f8f2;
  --zettlr-editor-selection-color: #44475a;
  --zettlr-editor-highlight-color: rgba(241, 250, 140, 0.35);
  --zettlr-editor-primary-color: #bd93f9;
  --zettlr-editor-secondary-color: #f1fa8c;
  --zettlr-editor-citation-color: #6272a4;
  --zettlr-editor-citation-bg: #21222c;
  --zettlr-editor-escape-color: #ffb86c;
  --zettlr-editor-accent-color: #282a36;
  --zettlr-editor-accent-bg: #bd93f9;
  --zettlr-editor-error-color: #ff5555;
  --zettlr-editor-code-color: #f8f8f2;
  --zettlr-editor-code-bg: #0f0f0f;

  background-color: #282a36 !important;
}

body.dark .cm-content {
  color: #f8f8f2 !important;
  caret-color: #f8f8f2 !important;
}

/* Active line uses a translucent overlay — Dracula's own spec recommendation */
body.dark .cm-activeLine {
  background-color: rgba(98, 114, 164, 0.15) !important;
}
body.dark .cm-selectionBackground {
  background-color: var(--zettlr-editor-selection-color) !important;
}

/* ================================
   Headings — size and weight only, no color assignment
   ================================ */
body.dark .cm-header-1 { font-size: 1.8em; font-weight: 700; }
body.dark .cm-header-2 { font-size: 1.5em; font-weight: 600; }
body.dark .cm-header-3 { font-size: 1.3em; font-weight: 600; }
body.dark .cm-header-4 { font-size: 1.15em; font-weight: 600; }
body.dark .cm-header-5 { font-size: 1.05em; font-weight: 500; }
body.dark .cm-header-6 { font-size: 1em; font-weight: 500; }

/* ================================
   Emphasis & links
   ================================ */
body.dark .cm-strong {
  color: #ffffff !important;
  font-weight: 700;
}
body.dark .cm-em {
  color: #f1fa8c !important;
  font-style: italic;
}
body.dark .cm-link,
body.dark .cm-url {
  color: #8be9fd !important;
  text-decoration: underline;
}

/* ================================
   Blockquotes
   ================================ */
body.dark .cm-quote {
  color: #c3c6e0 !important;
  font-style: italic;
  border-left: 4px solid #bd93f9;
  background-color: #21222c;
}

/* ================================
   Lists & horizontal rules
   ================================ */
body.dark .cm-list {
  color: #f8f8f2 !important;
}
body.dark .cm-hr {
  border-top: 1px solid #44475a !important;
}

/* ================================================================
   Fenced/inline code syntax highlighting — Dracula token mapping
   ================================================================ */
body.dark .cm-editor .tok-keyword,
body.dark .cm-editor .tok-controlKeyword,
body.dark .cm-editor .tok-moduleKeyword,
body.dark .cm-editor .tok-operatorKeyword,
body.dark .cm-editor .tok-definitionKeyword {
  color: #ff79c6 !important; /* Pink */
}
body.dark .cm-editor .tok-variableName.tok-function,
body.dark .cm-editor .tok-function {
  color: #50fa7b !important; /* Green */
}
body.dark .cm-editor .tok-typeName,
body.dark .cm-editor .tok-className,
body.dark .cm-editor .tok-namespace,
body.dark .cm-editor .tok-tagName,
body.dark .cm-editor .tok-attributeName {
  color: #8be9fd !important; /* Cyan */
}
body.dark .cm-editor .tok-string,
body.dark .cm-editor .tok-attributeValue,
body.dark .cm-editor .tok-regexp {
  color: #f1fa8c !important; /* Yellow */
}
body.dark .cm-editor .tok-number,
body.dark .cm-editor .tok-bool,
body.dark .cm-editor .tok-atom,
body.dark .cm-editor .tok-null {
  color: #ffb86c !important; /* Orange */
}
body.dark .cm-editor .tok-comment,
body.dark .cm-editor .tok-lineComment,
body.dark .cm-editor .tok-blockComment {
  color: #6272a4 !important; /* Comment — dimmed, italic */
  font-style: italic;
}
body.dark .cm-editor .tok-self {
  color: #bd93f9 !important; /* Purple — instance reserved words */
  font-style: italic;
}
body.dark .cm-editor .tok-invalid {
  color: #ff5555 !important; /* Red — wavy underline for parse errors */
  text-decoration: underline wavy;
}
body.dark .cm-editor .tok-variableName {
  color: #f8f8f2 !important; /* Foreground — plain variables stay neutral */
}

Breaking Down the CSS Section by Section

The Font Override

.cm-editor {
  font-family: Fira Code !important;
}

This is unscoped — it applies in both light and dark mode. Fira Code (YOu can install this custom font in Windows otherwise use default font, its ok!) is a monospaced font with programming ligatures that makes both code blocks and the markdown syntax itself easier to read. You can swap this for any monospace font you have installed: JetBrains Mono, Cascadia Code, Hack, whatever you prefer. The !important is necessary because Zettlr's default font is set with high specificity and a normal declaration won't override it.

If you don't have Fira Code installed, the fallback is your system's default monospace font. Go to fonts.google.com and search for "Fira Code" to download it, or install it via a package manager.

Editor Surface Variables

The --zettlr-editor-* custom properties are the cleanest way to configure the editor. Rather than hunting down every individual class that sets a color, you set the variable once and let Zettlr's existing styles consume it. The ones I found most important:

  • --zettlr-editor-scroller-bg — the main editor canvas background
  • --zettlr-editor-scroller-color — the default text color
  • --zettlr-editor-primary-color — used for accents like horizontal rules, YAML fence markers, and citation marks
  • --zettlr-editor-code-bg — the background chip behind inline code spans. I set this to #0f0f0f (near-black) rather than the Dracula background so inline code visually pops from the surrounding text

The background-color: #282a36 !important on .cm-editor is belt-and-suspenders insurance. Sometimes the variable doesn't propagate to the outermost element in certain Zettlr versions, so the explicit property keeps the background consistent.

Active Line and Selection

body.dark .cm-activeLine {
  background-color: rgba(98, 114, 164, 0.15) !important;
}

This is the line your cursor is currently on. The Dracula spec recommends using the "Current Line" color (#44475a) as a transparent overlay rather than a flat fill — it gives you a subtle highlight without making the line feel like it's on a different surface. I used rgba(98, 114, 164, 0.15) which is Dracula's Comment color (#6272a4) converted to RGBA with 15% opacity. The effect is just perceptible enough to orient you in a long document without being distracting.

Headings

The heading rules deliberately don't set color. Most Zettlr themes assign a rainbow of Dracula colors to heading levels, and while this looks appealing in a demo, in practice it creates visual noise when you're writing. Your brain starts associating colors with heading hierarchy rather than reading the content. Size and weight are enough to establish hierarchy, and they work in both light and dark mode without needing separate rules.

Syntax Highlighting in Code Blocks

This is the most complex part. Zettlr uses CodeMirror 6's @codemirror/language package for code block highlighting, which uses tok-* class names derived from the Lezer grammar system. These class names are relatively stable but not guaranteed by Zettlr as a public API. If you write a code block in a specific language and a token isn't picking up the color you expect, right-click the token in the editor, choose Inspect Element, find the actual class name applied, and add a rule for it.

The mapping I used follows Dracula's own token spec exactly:

  • Pink for keywords (if, for, import, class)
  • Green for function names
  • Cyan for types, class names, HTML tags, and attributes
  • Yellow for strings and regular expressions
  • Orange for numbers, booleans, and null
  • Comment color (muted blue-gray) for comments, italicised

Installing Fira Code (If You Haven't Already)

On Windows, download the latest release from github.com/tonsky/FiraCode, extract the zip, select all .ttf files in the ttf folder, right-click, and choose Install for all users.

On macOS, you can use Homebrew:

brew install font-fira-code

On Linux (Debian/Ubuntu):

sudo apt install fonts-firacode

After installing, close and reopen Zettlr. The font override in the CSS will take effect on the next launch.


A Few Things to Know Before You Use This

It only applies in dark mode. Everything is scoped under body.dark, which is the class Zettlr adds to the body when dark mode is active. If you switch to light mode, you get the default Zettlr light theme unchanged. If you want a custom light theme too, you'd write separate rules without the body.dark scope.

The tok-* classes are semi-stable. Zettlr doesn't officially document CodeMirror token class names as part of its public API. They've been consistent across several major versions, but after a significant Zettlr update, check your code blocks to make sure syntax highlighting still looks right.

You can combine this with Zettlr's built-in themes. The custom CSS applies on top of whatever base theme you have selected. I use it on top of the default "Berlin" dark theme. Some of the variable overrides may behave differently if you're using a third-party base theme.

Zettlr does not have a live theme preview. Each time you paste updated CSS and click save, give it a second — the app applies it immediately, but occasionally needs a moment to re-render. If something looks wrong, try switching files and switching back.


Why I Kept Coming Back to This Setup

I've refined this theme probably a dozen times since I first put it together. The version in this post is what I've settled on after months of actual use — not just how it looks in a screenshot, but how it feels after three hours of continuous writing or deep research sessions.

The biggest thing that keeps me here is that the editor feels like it disappears when I'm working in it. The Dracula background is dark enough to reduce eye strain, the text has enough contrast to be comfortable, and the syntax highlighting in code blocks is clear without being loud. It's the kind of setup where you stop noticing the tool and start noticing the work.

Zettlr won't replace your terminal or your IDE. But for writing, connecting ideas, and building a second brain from plain text files, it's one of the most underrated tools available — and a good theme makes it significantly better.