@hadoku/themes
v5.14.4
Published
16 themes with a symmetric, WCAG-verified token set and a complete Tailwind v4 color mapping - Light, Dark, Ocean, Cyberpunk, Coffee, Lavender, Nature, Pink, Izakaya
Maintainers
Readme
@wolffm/themes
Beautiful color themes using CSS custom properties. Framework-agnostic and ready to use in any web project.
Themes
8 theme families (16 themes), each with light and dark variants:
- Light (default) / Dark - Clean blue and white / Sophisticated midnight with purple accents
- Ocean Light/Dark - Deep sea blues and tropical coral
- Cyberpunk Light/Dark - Neon dystopia with electric colors
- Coffee Light/Dark - Rich espresso and cream tones
- Lavender Light/Dark - Soft purple elegance
- Nature Light/Dark - Lush leaf greens inspired by forests and earth
- Pink Light/Dark - Bubblegum pink and neon glows
- Izakaya Light/Dark - Japanese izakaya-inspired warm tones (Kitsune contest winner)
Installation
npm install @wolffm/themesUsage
Import CSS
import '@wolffm/themes/style.css'Use Theme Utilities
import { setTheme, loadTheme, saveTheme, THEMES } from '@wolffm/themes'
// Set theme (applies immediately)
setTheme('dark')
setTheme('cyberpunk-dark')
// Save to sessionStorage and apply
saveTheme('lavender-light')
// Load saved theme on app start
const theme = loadTheme()
// Get all available themes
console.log(THEMES) // ['light', 'dark', 'coffee-light', ...]Apply to HTML
<!-- Set globally on html element -->
<html data-theme="dark">
<!-- Or on a specific container -->
<div data-theme="ocean-light">
<!-- Your content -->
</div>
</html>Paint in the visitor's theme before the first frame
A page that sets data-theme from a bundle paints once untinted and then
swaps. @wolffm/themes publishes the one pre-paint script every hadoku page
inlines at the top of <head>: it reads the stored choice (or deals a sticky
random dark theme to someone who never picked one), sets data-theme before
anything paints, and publishes the decision as window.__hadokuTheme. The
header of src/theme-bootstrap.js documents what it resolves and exposes.
It must be inline text. A <script src>, a module, or a bundled script all
run after first paint, which defeats the point. Take it one of two ways:
// As a string, for a build tool. Astro:
import { THEME_BOOTSTRAP } from '@wolffm/themes/theme-bootstrap'
// <script is:inline set:html={THEME_BOOTSTRAP} />// As the file's text, for a sync script that writes it between markers:
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
const path = createRequire(import.meta.url).resolve('@wolffm/themes/theme-bootstrap.js')
const source = readFileSync(path, 'utf8')The text holds no closing script tag and no <!--, so it can go inside a
<script> element unescaped (test/theme-bootstrap.test.mjs enforces it). It
calls /session/whoami and /prefs/api/v1/portfolio by relative URL, so its
cross-device half (remember(), adopt()) only works on hadoku.me's origin.
Never copy it into a repo: import it, so a new theme reaches every page
with a version bump.
CSS Variables
Each theme defines 47 variables (41 color + 6 shadow). Color variables use the
--color-* namespace — which is also Tailwind v4's — so they map straight to
utilities. Everything else is namespaced --hdk-* to avoid colliding with
Tailwind internals.
Which token do I use for X? →
THEME_USAGE_GUIDE.md. That is the doc to read (and to point agents at) before writing any styles.
Colors — 5 semantic families × 6 tokens
Every family carries an identical set. The rectangle is the point: the symmetric guess is always correct.
| | fill | gradient/pressed | tint | hover overlay | text on fill | text on tint |
| --------- | ----------------- | ---------------- | ----- | ------------- | -------------------- | ----------------------- |
| primary | --color-primary | -dark | -bg | -hover | --color-on-primary | --color-on-primary-bg |
| success | --color-success | -dark | -bg | -hover | --color-on-success | --color-on-success-bg |
| warning | --color-warning | -dark | -bg | -hover | --color-on-warning | --color-on-warning-bg |
| danger | --color-danger | -dark | -bg | -hover | --color-on-danger | --color-on-danger-bg |
| neutral | --color-neutral | -dark | -bg | -hover | --color-on-neutral | --color-on-neutral-bg |
Colors — structural
--color-text(+-secondary,-tertiary,-muted)--color-border(+-light)--color-bg(+-card,-alt,-hover,-overlay)
Every on-* pair is verified ≥4.5:1 against its surface in every theme by
scripts/check-contrast.mjs.
Typography
--font-family--hdk-text-*(xs, sm, md, base, lg) - font sizes--font-weight-*(normal, semibold, bold)--line-height-*(normal, relaxed)
Spacing
--hdk-space-*(xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl)
Layout
--hdk-radius(+ sm, lg variants) - border radius--hdk-shadow-*(sm, md, lg, focus, focus-sm, focus-alt)
Transitions
--transition-fast--transition-smooth
Usage Example
.my-button {
background: var(--color-primary);
color: var(--color-on-primary);
padding: var(--hdk-space-md) var(--hdk-space-xl);
border-radius: var(--hdk-radius);
font-size: var(--hdk-text-base);
box-shadow: var(--hdk-shadow-sm);
transition: var(--transition-fast);
}
.my-button:hover {
background: var(--color-primary-dark);
box-shadow: var(--hdk-shadow-md);
}Tailwind v4 Compatibility
This package uses namespaced CSS variables (--hdk-*) to avoid collisions with Tailwind v4's internal variables.
Why Namespacing?
Tailwind v4 uses CSS custom properties internally:
--spacing-*powersmax-w-md,p-4,gap-*, etc.--radius-*powersrounded-md,rounded-lg, etc.--font-size-*powerstext-sm,text-base, etc.--shadow-*powersshadow-sm,shadow-md, etc.
Without namespacing, a theme variable like --spacing-md: 8px would break max-w-md (changing it from 28rem to 8px).
Variable Mapping
| Old Name (v1.x) | New Name (v2.x) |
| ----------------- | ----------------- |
| --spacing-md | --hdk-space-md |
| --font-size-sm | --hdk-text-sm |
| --border-radius | --hdk-radius |
| --shadow-sm | --hdk-shadow-sm |
| --shadow-modal | --hdk-shadow-lg |
Tailwind Integration
Three imports, no hand-written @theme block:
@import '@wolffm/themes/style.css'; /* 1. tokens — MUST be unlayered */
@import 'tailwindcss'; /* 2. Tailwind */
@import '@wolffm/themes/tailwind-colors.css'; /* 3. all 41 colours */
@import '@wolffm/themes/tailwind-integration.css'; /* 4. radius/shadow/type (optional) */You now get bg-primary, text-on-primary, bg-success-bg,
text-on-danger-bg, border-border, bg-bg-card — every token, as a utility.
Never hand-roll a @theme colour block. A local copy is exactly the drift
this package exists to prevent; tailwind-colors.css is generated from
style.css and CI fails if the two disagree.
Step 1 must stay unlayered. Tailwind emits each @theme entry into
@layer theme as :root { --x: var(--x) }, which is self-referential and
invalid on its own. It resolves only because style.css declares the same
properties outside any cascade layer, and unlayered wins. Import it with
layer(...) and all 41 colours silently become transparent.
tailwind-integration.css (step 4) is separate and optional — it maps
--hdk-* radius/shadow/type onto rounded-*, shadow-*, text-*.
Migration from v1.x
Search and replace in your CSS:
var(--spacing- → var(--hdk-space-
var(--font-size- → var(--hdk-text-
var(--border-radius → var(--hdk-radius
var(--shadow- → var(--hdk-shadow-
var(--shadow-modal) → var(--hdk-shadow-lg)React Integration
Using the useTheme Hook (Recommended)
import { useTheme, THEME_FAMILIES, THEME_ICON_MAP } from '@wolffm/themes'
import { ConnectedThemePicker } from '@wolffm/task-ui-components'
import '@wolffm/themes/style.css'
import '@wolffm/task-ui-components/theme-picker.css'
function App() {
const { theme, setTheme } = useTheme()
return (
<ConnectedThemePicker
themeFamilies={THEME_FAMILIES}
currentTheme={theme}
onThemeChange={setTheme}
getThemeIcon={theme => {
const Icon = THEME_ICON_MAP[theme]
return Icon ? <Icon /> : null
}}
/>
)
}Manual React Integration
import { useEffect, useState } from 'react'
import { loadTheme, setTheme, THEMES, type Theme } from '@wolffm/themes'
import '@wolffm/themes/style.css'
function App() {
const [theme, setThemeState] = useState<Theme>('light')
useEffect(() => {
const savedTheme = loadTheme()
setThemeState(savedTheme)
}, [])
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
setThemeState(newTheme)
}
return (
<select value={theme} onChange={e => handleThemeChange(e.target.value as Theme)}>
{THEMES.map(t => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
)
}Framework Examples
Vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { loadTheme, setTheme, THEMES, type Theme } from '@wolffm/themes'
import '@wolffm/themes/style.css'
const theme = ref<Theme>('light')
onMounted(() => {
theme.value = loadTheme()
})
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
theme.value = newTheme
}
</script>
<template>
<select v-model="theme" @change="handleThemeChange(theme)">
<option v-for="t in THEMES" :key="t" :value="t">{{ t }}</option>
</select>
</template>Vanilla JS
import { loadTheme, setTheme, THEMES } from '@wolffm/themes'
import '@wolffm/themes/style.css'
// Load saved theme on page load
document.addEventListener('DOMContentLoaded', () => {
loadTheme()
// Theme switcher
const select = document.getElementById('theme-select')
THEMES.forEach(theme => {
const option = document.createElement('option')
option.value = theme
option.textContent = theme
select.appendChild(option)
})
select.addEventListener('change', e => {
setTheme(e.target.value)
})
})Theme Editor
Hosted at https://hadoku.me/task/editor (service tier and up). Pick a theme,
drag sliders, and the app's real components recolour live; Save commits the
change to main under a new version, which publishes.
- Per token — brightness / saturation / hue / opacity sliders (OKLCH, so a brightness step looks even across hues) plus a typed value
- Per group — Surfaces, Text and each semantic family can be shifted at once
- Contract — the same checks
lint:cssruns (scripts/lib/contract.mjs, exported as@wolffm/themes/contract), live; a failure blocks Save, and the save route re-checks before committing - Comfort — advisory meters for what AA cannot see: page glare, card-vs-page
separation, border visibility, text harshness, and whether
success/warning/dangerstill read as their meaning
The editor ships as @wolffm/task/editor (src/editor/ in the repo root); its
save route is worker/src/routes/theme-editor.ts. Locally, pnpm run dev:api
plus vite serve it at /editor.html against an in-memory GitHub — a local
save never touches the real repo.
API Reference
React Utilities (New in v1.1.3+)
useTheme(): { theme: Theme, setTheme: (theme: Theme) => void }
React hook for stateful theme management. Automatically saves to sessionStorage and syncs across tabs.
const { theme, setTheme } = useTheme()THEME_FAMILIES: ThemeFamily[]
Pre-configured array of all 8 theme families with icons and labels. Use with ThemePicker or ConnectedThemePicker components from @wolffm/task-ui-components.
import { THEME_FAMILIES } from '@wolffm/themes'
// Each family includes: lightTheme, darkTheme, lightLabel, darkLabel, lightIcon, darkIconTHEME_ICON_MAP: Record<Theme, IconComponent>
Map of theme names to their corresponding icon components.
import { THEME_ICON_MAP } from '@wolffm/themes'
const Icon = THEME_ICON_MAP['cyberpunk-dark']Core Functions
setTheme(theme: Theme): void
Apply a theme immediately. Sets data-theme attribute on <html> element.
getTheme(): Theme
Get the currently active theme.
saveTheme(theme: Theme): void
Save theme to sessionStorage and apply it.
loadTheme(): Theme
Load saved theme from sessionStorage, apply it, and return the theme name. Respects browser's prefers-color-scheme preference if no saved theme. Returns 'light' if no preference.
initTheme(): Theme
Convenience function. Same as loadTheme().
clearTheme(): void
Remove saved theme and reset to 'light'.
Types
type Theme =
| 'light'
| 'dark'
| 'ocean-light'
| 'ocean-dark'
| 'cyberpunk-light'
| 'cyberpunk-dark'
| 'coffee-light'
| 'coffee-dark'
| 'lavender-light'
| 'lavender-dark'
| 'nature-light'
| 'nature-dark'
| 'pink-light'
| 'pink-dark'
| 'izakaya-light'
| 'izakaya-dark'
const THEMES: readonly Theme[]Browser Support
Works in all modern browsers that support:
- CSS Custom Properties (CSS Variables)
- ES2022 modules
- sessionStorage
License
MIT
Author
WolffM - GitHub
Related Packages
- @wolffm/task - Task management micro-frontend using these themes
