Skip to content

Theming

Quiver ships with five distinct visual themes, each supporting light and dark mode independently. Theme and mode are stored in uiState and persisted to localStorage.


How it works

Two orthogonal attributes live on <html>:

html
<html data-theme="default" data-mode="dark">
  • data-theme — visual identity: default · mono · glass · retro · brutalist
  • data-mode — brightness: light · dark

Tailwind's @variant dark targets [data-mode=dark], so changing the mode switches the dark token set without affecting the theme.

An inline script in index.html reads both values from localStorage before any CSS or JavaScript loads, preventing a flash of the wrong theme on page load.


The five themes

Default

Orange brand, slate neutrals. The original Quiver look. No visual change from before the theming system was added.

Monochrome

Zero hue. Pure white/black surfaces, near-black/white text, black/white brand. Tight letter-spacing on headings, sharp radii, minimal shadows. Inspired by shadcn/ui.

Liquid Glass

Apple-inspired frosted glass. backdrop-filter: blur(20px) saturate(160%) on panels and cards with a translucent surface colour. Soft violet brand, large radii, diffuse shadows. Requires a background behind cards to be visible — the page background gradient provides this.

Retro / Y2K

1980s arcade aesthetic. VT323 monospace font, hot-pink brand, neon text-shadows on headings, neon box-shadows on panels. A full-screen scanline overlay (html::after) adds the CRT monitor effect. Dark variant deepens to near-black with electric-pink neon.

Neo Brutalism

High-contrast editorial. Space Grotesk 900-weight headings in all-caps. Solid 3px borders on panels, yellow brand, offset drop-shadow (4px 4px 0). Buttons snap on press (translate(2px, 2px) with shrunk shadow). Zero border-radius throughout.


Changing theme or mode

Mutate uiState.theme or uiState.mode directly:

js
import { uiState } from '../state/uiState.js'

uiState.theme = 'glass'    // switch visual identity
uiState.mode  = 'dark'     // switch brightness

The watch() in uiState.js syncs both values to data-theme/data-mode on <html> and writes them to localStorage automatically.

The built-in UI provides:

  • ThemeSelector — five swatch buttons in the MenuLayout header; sets uiState.theme
  • ThemeToggle — pill toggle in the MenuLayout header; flips uiState.mode

Semantic design tokens

All components use semantic CSS custom properties instead of palette utilities. Tokens are defined in src/style.css under @theme and overridden per theme and mode.

Color tokens

TokenUtility classRole
--color-surfacebg-surfacePage background
--color-surface-raisedbg-surface-raisedCards, sidebars, header
--color-surface-insetbg-surface-insetInput backgrounds, code blocks
--color-fgtext-fgHeadings and body copy
--color-fg-softtext-fg-softSecondary labels
--color-fg-fainttext-fg-faintPlaceholder, disabled, metadata
--color-lineborder-lineDefault dividers
--color-line-strongborder-line-strongEmphasis borders
--color-brandtext-brand / bg-brandPrimary action colour
--color-brand-tintbg-brand-tintTinted badge backgrounds

Shape and shadow tokens

TokenUtilityRole
--radius-controlrounded-controlButtons, inputs, badges
--radius-panelrounded-panelCards, modals, panels
--shadow-panelshadow-panelCard elevation
--shadow-floatshadow-floatPopover / dropdown elevation

Font tokens

TokenRole
--font-displayHeadings; varies by theme
--font-uiBody / UI text
--font-monoCode and metadata

Per-theme variant classes

Tailwind variants are registered for each non-default theme:

theme-glass:   [data-theme="glass"] &
theme-retro:   [data-theme="retro"] &
theme-mono:    [data-theme="mono"] &
theme-brutalist: [data-theme="brutalist"] &

Use these to apply theme-specific overrides directly in markup:

js
html`
  <div class="rounded-panel border border-line bg-surface-raised shadow-panel
              theme-glass:backdrop-blur-md theme-brutalist:border-2">
    ...
  </div>
`

Adding a new theme

  1. Choose an identifier, e.g. 'ocean'.

  2. Add a token override block in src/style.css:

    css
    /* Ocean — light */
    [data-theme="ocean"]:where([data-mode="light"], [data-mode="light"] *) {
      --color-surface: oklch(97% 0.02 220);
      --color-brand: oklch(55% 0.18 220);
      /* …remaining tokens */
    }
    
    /* Ocean — dark */
    [data-theme="ocean"]:where([data-mode="dark"], [data-mode="dark"] *) {
      --color-surface: oklch(15% 0.03 220);
      --color-brand: oklch(65% 0.18 220);
    }
  3. Register a Tailwind variant if you need per-theme utility overrides:

    css
    @variant theme-ocean (&:where([data-theme="ocean"], [data-theme="ocean"] *));
  4. Add the theme to the THEMES array in src/components/ThemeSelector.js:

    js
    { id: 'ocean', label: 'Ocean', bg: 'bg-[#0ea5e9]' },
  5. Add an entry point in the anti-flash script in index.html if the new identifier needs special handling (usually not required).