# How to Implement a Custom Theme System in Hallmark: A Complete Guide

> Learn to implement a custom theme system in Hallmark with this guide. Discover how to use CSS custom properties and a data-theme attribute for easy theming.

- Repository: [Hassan El Mghari/hallmark](https://github.com/Nutlope/hallmark)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/site/js/main.js) | Theme switching logic + UI updates | Selectors at 546–547 and 746 |
| [`site/css/components.css`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/tokens.css)

Open [`site/css/tokens.css`](https://github.com/Nutlope/hallmark/blob/main/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`

```css
/* 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`](https://github.com/Nutlope/hallmark/blob/main/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:

```html
<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`](https://github.com/Nutlope/hallmark/blob/main/site/css/components.css) reference tokens, not hardcoded values. Confirm your theme applies by checking a representative component:

```css
/* 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`](https://github.com/Nutlope/hallmark/blob/main/main.js)

Understanding the JavaScript helps debug issues. The core logic, paraphrased from **lines 546–547 and 746** of [`main.js`](https://github.com/Nutlope/hallmark/blob/main/main.js):

```javascript
// 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`](https://github.com/Nutlope/hallmark/blob/main/site/css/sections.css) contains additional `[data-theme]` blocks scoped to section classes. Follow this pattern when a theme needs layout-specific adjustments:

```css
/* 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`](https://github.com/Nutlope/hallmark/blob/main/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.css`](https://github.com/Nutlope/hallmark/blob/main/tokens.css) for values, [`main.js`](https://github.com/Nutlope/hallmark/blob/main/main.js) for switching logic, and [`components.css`](https://github.com/Nutlope/hallmark/blob/main/components.css) for consumption.
- **Adding a theme** means: (1) write a token block in [`tokens.css`](https://github.com/Nutlope/hallmark/blob/main/tokens.css), (2) add a `data-theme-btn` button, (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`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/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`](https://github.com/Nutlope/hallmark/blob/main/main.js) where it checks `localStorage` and falls back to a default.