How to Implement a Custom Theme System in Hallmark: A Complete Guide
Hallmark implements theming through CSS custom properties scoped to a [data-theme="…"] attribute on the <html> element, requiring only three steps: define token overrides in tokens.css, add a data-theme-btn button to the UI, and let the existing JavaScript handle the switch.
If you're building with Hallmark — the elegant static card generator by Nutlope/hallmark — you may want to extend its visual palette beyond the twelve built-in themes. The good news: the entire theming layer is client-side, requires no build tools, and follows a predictable token-based architecture. This guide walks through the exact implementation based on the source code.
How Hallmark's Theme System Works Under the Hood
The architecture rests on two pillars: CSS custom properties for values and a data attribute selector for scoping. Every theme is a pure CSS block that redefines variables inside [data-theme="name"] { … }.
Core Files and Responsibilities
| File | Purpose | Key Lines |
|---|---|---|
site/css/tokens.css |
Base design tokens + all theme overrides | Theme blocks start at 216, 268, 316, 365, 411, 461, 517, 575, 617, 664, 709, 753, 793 |
site/js/main.js |
Theme switching logic + UI updates | Selectors at 546–547 and 746 |
site/css/components.css |
Component styles consuming tokens | Uses --radius-card, --shadow-card, etc. |
The separation is deliberate: tokens describe what changes, components describe how elements look, and JavaScript handles when to change.
Step 1: Define Your Theme Tokens in tokens.css
Open site/css/tokens.css and add a new block following the existing pattern. The Garden theme at line 216 is the canonical reference.
Each token block redefines the subset of variables you want to override. At minimum, target these categories:
- Palette:
--paper,--accent,--text,--muted,--border - Shape:
--radius-card,--radius-pill,--radius-sm - Depth:
--shadow-card,--shadow-float,--shadow-glow
/* site/css/tokens.css — new "midnight" theme */
[data-theme="midnight"] {
/* Palette: deep navy with amber accent */
--paper: oklch(15% 0.02 260);
--ink: oklch(95% 0.01 260);
--accent: oklch(70% 0.15 80);
--muted: oklch(60% 0.03 260);
--border: oklch(30% 0.04 260);
/* Shape */
--radius-card: 8px;
--radius-pill: 999px;
/* Depth */
--shadow-card: 0 4px 12px oklch(0% 0 0 / 0.25);
--shadow-float: 0 8px 24px oklch(0% 0 0 / 0.35);
}
Pro tip: Use oklch() for perceptually uniform colors, as Hallmark's built-in themes do. This ensures consistent lightness across hues.
Step 2: Add a Theme Switch Button
The JavaScript in main.js automatically discovers buttons via the [data-theme-btn] attribute. No registration code required.
Add this to your HTML wherever the theme picker lives:
<button data-theme-btn
data-theme="midnight"
aria-label="Activate Midnight theme"
aria-pressed="false">
🌙
</button>
The data-theme value must match your CSS block name exactly. The aria-pressed state is toggled by the script to indicate the active theme.
Step 3: Verify Component Adaptation
Components in site/css/components.css reference tokens, not hardcoded values. Confirm your theme applies by checking a representative component:
/* From site/css/components.css — card component */
.card {
background: var(--paper);
color: var(--ink);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
border: 1px solid var(--border);
}
Because .card never declares concrete values, it instantly reflects your --paper, --shadow-card, and other overrides when [data-theme="midnight"] is active on <html>.
The Theme Switching Mechanism in main.js
Understanding the JavaScript helps debug issues. The core logic, paraphrased from lines 546–547 and 746 of main.js:
// Select all theme buttons and current-label elements
const themeButtons = document.querySelectorAll("[data-theme-btn]");
const currentLabel = document.querySelector("[data-theme-current]");
const footerLabel = document.querySelector("[data-theme-current-foot]");
themeButtons.forEach(button => {
button.addEventListener("click", () => {
const chosenTheme = button.dataset.theme;
// Apply theme to root element — this triggers CSS changes
document.documentElement.dataset.theme = chosenTheme;
// Update UI labels
currentLabel.textContent = chosenTheme;
if (footerLabel) footerLabel.textContent = chosenTheme;
// Manage aria-pressed for accessibility
themeButtons.forEach(b => b.setAttribute("aria-pressed", "false"));
button.setAttribute("aria-pressed", "true");
});
});
The real file adds keyboard navigation (arrow keys between theme buttons) and persists the choice to localStorage, but this excerpt captures the essential contract between HTML, CSS, and JavaScript.
Extending Themes: Advanced Patterns
Per-Section Theme Overrides
For card designs that need section-specific tuning, site/css/sections.css contains additional [data-theme] blocks scoped to section classes. Follow this pattern when a theme needs layout-specific adjustments:
/* Override hero spacing only in Midnight theme */
[data-theme="midnight"] .hero-section {
--hero-padding: var(--space-xl);
background-image: linear-gradient(to bottom, var(--paper), transparent);
}
Preview Images in the Theme Picker
The built-in themes include visual previews. To add yours, reference the existing structure in the HTML — typically an <img> or CSS background inside the button — and ensure the asset path resolves from site/.
Debugging Custom Themes
| Symptom | Likely Cause | Fix |
|---|---|---|
| Theme not applying | Typo in data-theme value vs. CSS block name |
Verify exact match, including case |
| Variables fall back to browser defaults | Token not defined in base or theme block | Define in [data-theme="your-theme"] or ensure inheritance from base |
| Flash of unstyled theme on load | localStorage theme applied after paint |
The production main.js handles this; ensure your fork preserves the early script execution |
| Button clicks do nothing | Missing data-theme-btn attribute |
Add to button element |
Summary
- Hallmark's theme system uses CSS custom properties scoped by
[data-theme]attributes — no preprocessor or server logic required. - Three files matter:
tokens.cssfor values,main.jsfor switching logic, andcomponents.cssfor consumption. - Adding a theme means: (1) write a token block in
tokens.css, (2) add adata-theme-btnbutton, (3) verify components adapt. - Component resilience comes from pure token references — your theme automatically works across all UI elements.
Frequently Asked Questions
Do I need to rebuild or redeploy Hallmark to add a theme?
No. Hallmark is a static site. Edit site/css/tokens.css and the HTML directly, then refresh. The theme system is entirely client-side with no build step dependency.
Can I override a single component without changing the global theme?
Yes, but respect the architecture. Add a scoped selector like [data-theme="custom"] .specific-component { … } in sections.css or a new file. Avoid inline styles; they break the token contract.
Why does Hallmark use oklch() instead of hex or HSL?
oklch() provides perceptually uniform lightness. When you change hues, the perceived brightness stays consistent — critical for accessible contrast ratios. The source code in tokens.css models this practice throughout.
How do I make my custom theme the default?
Set data-theme="your-theme" directly on the <html> element in your HTML template, or modify the initialization logic in main.js where it checks localStorage and falls back to a default.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →