Loom ships with 6 built-in themes. Every theme supports light and dark mode, switches instantly via hot reload, and ships inlined — zero external requests.

This post walks through every layer of customization, from the simplest one-line change to building your own theme from scratch.

Pick a Theme

Open site.conf and set the theme key:

theme = gruvbox

Save. If Loom is running, the change is live immediately — no restart needed.

Here’s every available theme:

ThemeVibe
defaultClean, neutral grays, blue accent
terminalMonospace hacker aesthetic, green accent, sharp corners
nordArctic frost palette, muted blues
gruvboxRetro groove, warm earthy contrast, orange accent
roseSoft pinks, magenta accent, round corners
hackerCRT phosphor green-on-black, scanlines, custom components

Each theme defines a light palette, a dark palette, a font stack, font size, and content width. But themes go beyond colors — they also define structural choices that change the shape and behavior of UI components:

  • Corners — Soft (rounded), Sharp (square), or Round (pill-shaped). Affects every element site-wide.
  • Tag style — Pill badges, bordered rectangles, outlined with muted borders, or plain text.
  • Link style — Solid underline, dotted, dashed, or none.
  • Headings — Normal, UPPERCASE, or lowercase.

All code styling is palette-driven — code blocks use var(--text) on a subtle var(--bg) tint, inline code uses var(--accent) on an accent-tinted background. Define your five palette colors and code rendering works in both light and dark mode automatically. No separate light/dark paths needed.

This is why terminal and rose feel completely different — terminal uses sharp corners, bordered tags, and no link decoration, while rose uses round corners, pill-shaped tags, and pill-shaped nav links. Themes can also override content defaults like date format and index headings without writing component code.

Override Individual Variables

You don’t need to write CSS. Every theme is built on CSS custom properties. Override any of them from site.conf:

theme = nord
theme_accent = #e06c75
theme_dark-accent = #e06c75

The naming rule is simple: theme_ + the variable name, with hyphens written as underscores. For dark-mode-only overrides, prefix the variable with dark-:

theme_font_size = 18px        # affects both modes
theme_max_width = 800px       # affects both modes
theme_dark-bg = #000000       # only changes dark mode background

What Can You Override?

Everything. Here are the most useful variables:

Colors — the five core tokens:

VariableWhat it controls
bgPage background
textPrimary text
mutedDates, meta text, secondary content
borderLines, separators, card borders
accentLinks, active states, highlights

Typography:

VariableDefaultWhat it controls
fontSystem sans-serifBody font stack
font-size17pxBase text size
heading-fontinheritHeading font (set to a different family)
heading-weight700Heading boldness
line-height1.7Text line spacing
code-fontMonospace stackFont for code blocks

Layout:

VariableDefaultWhat it controls
max-width700pxContent column width
content-widthSame as max-widthOverall container width
sidebar-width260pxSidebar column width
sidebar-gap32pxSpace between content and sidebar
container-padding40px 20pxPage padding

Components:

VariableWhat it controls
border-radiusDefault corner rounding
card-bg, card-border, card-radius, card-paddingPost cards (when using post_list_style = cards)
tag-bg, tag-text, tag-radiusTag badges
tag-hover-bg, tag-hover-textTag hover state
header-border-widthHeader bottom line (set 0 to remove)
footer-border-widthFooter top line
link-decorationLink underline style

Every variable has a dark- counterpart. Override bg to change the light background; override dark-bg to change the dark background independently.

Example: Nord with Warm Accents

theme = nord
theme_accent = #bf616a
theme_dark-accent = #bf616a
theme_heading_font = Georgia, serif
theme_heading_weight = 600

This gives you Nord’s arctic palette with rosy accent links and serif headings. Three lines, no CSS.

Example: Make Any Theme Wider

theme = nord
theme_max_width = 900px
theme_font_size = 16px

Custom CSS

For changes that go beyond variables — say you want to restyle blockquotes or add a hover animation — use custom_css in site.conf:

custom_css = .post-card:hover { transform: scale(1.02); } .post-content blockquote { border-left-color: orange; }

This CSS is injected after the theme, so it wins any specificity ties. It’s a single line in site.conf — keep it short. For anything longer, use a full CSS override.

Full CSS Override

Drop a style.css file in content/theme/:

content/
  theme/
    style.css

This replaces the entire default stylesheet. You’re starting from zero — you own all layout, typography, responsive design, and dark mode handling. Loom detects the file automatically and stops using the built-in CSS.

This is the nuclear option. Most people never need it. Variable overrides and custom_css cover 95% of cases.

Injecting External Resources

Need Google Fonts, analytics, or custom meta tags? Use custom_head_html:

custom_head_html = <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">

Then reference the font via a variable override:

theme_font = Inter, system-ui, sans-serif

This is the only way to make an external request — Loom itself never reaches out to third-party servers.

How It All Works

Every page Loom serves is a single self-contained HTML document. Here’s the CSS injection order:

  1. Default CSS — the full base stylesheet with all layout, typography, and responsive rules
  2. Theme CSS — the selected theme’s color variables and extra CSS (overrides defaults)
  3. Variable overrides — your theme_* keys from site.conf (overrides the theme)
  4. Custom CSS — your custom_css line (overrides everything)

If you drop a style.css file in content/theme/, step 1 is replaced entirely — your file is used instead of the built-in CSS. Steps 2-4 still apply on top.

Dark mode works via a data-theme="dark" attribute on <html>. A tiny inline script in <head> checks localStorage first (for returning visitors), then falls back to prefers-color-scheme, then defaults to light. The script runs before paint — no flash of the wrong theme.

The toggle button swaps the attribute and saves the choice to localStorage. Everything is CSS — no JavaScript framework, no runtime, no FOUC.

Quick Reference

I want to…Do this
Change the themetheme = name in site.conf
Change one colortheme_accent = #hexvalue
Change dark mode onlytheme_dark-accent = #hexvalue
Change the fonttheme_font = YourFont, fallback
Make it widertheme_max_width = 900px
Remove header bordertheme_header_border_width = 0
Restyle a componentcustom_css = .selector { ... }
Add Google Fontscustom_head_html = <link ...>
Replace everythingDrop content/theme/style.css