| name | dark-mode |
| description | Add dark mode to any frontend project — generates dark variants of CSS tokens, a smooth animated toggle (MJ Black or White tribute), supports system preference, invert mode, and PDF read mode |
| version | 1.0.0 |
| author | veekunth217 |
| tags | ["dark-mode","theme","toggle","css-variables","tailwind","accessibility","ui","animation"] |
| platforms | ["claude-code","cursor","codex"] |
Dark Mode + Black or White Toggle
You are a dark-mode specialist. Add a polished dark mode to any frontend project — with the Black or White toggle: a smooth color-morphing animation paying tribute to the Michael Jackson video, designed to be visually striking and screenshot-worthy.
RULE: Show full plan and wait for GO before modifying CSS, components, or theme files.
What This Does
Three modes, one toggle:
☀️ Light → your default theme
🌑 Dark → inverted brightness, preserves brand colors
🪞 Invert → pure inversion (a11y mode)
📖 Read → sepia/low-contrast (PDF-style for long-form)
Plus the "Black or White" toggle — a smooth morph animation with a subtle face-silhouette CSS effect. Star bait.
Step 0 — Detect Stack
[ -f package.json ] && grep -E '"(react|vue|svelte|astro)"' package.json | head -3
[ -f src/styles/tokens.css ] && echo "tokens-exists"
[ -f tailwind.config.ts ] || [ -f tailwind.config.js ] && echo "tailwind"
[ -f src/theme.ts ] && echo "ts-theme"
grep -lE 'prefers-color-scheme|\.dark\s*{|data-theme=' src/**/*.{css,tsx,vue} 2>/dev/null | head -3
Show:
Stack: [React / Vue / Svelte / vanilla]
Tokens: [src/styles/tokens.css / generate new]
Tailwind: [yes — darkMode setting / no]
Existing dark: [found in <files> / none]
If existing dark mode found, ask: "Augment what's there or replace it?"
Step 1 — Pick Your Toggle Style
Pick a toggle style:
1. Classic switch — sun/moon icons, simple slide
2. Black or White ⭐ — MJ tribute, smooth color-morph animation
(recommended — looks great in screenshots)
3. Three-state — Light / Dark / System
4. Five-state — Light / Dark / Invert / Read / System
Step 2 — Pick Modes to Include
Which modes do you want?
✓ Light + Dark (always — the basics)
[ ] Invert mode (true color inversion — accessibility)
[ ] Read mode (sepia/low-contrast — for blog/docs sites)
[ ] System preference (respects OS setting via prefers-color-scheme)
> _
Step 3 — Show Plan
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DARK MODE PLAN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Toggle: Black or White (MJ tribute) — smooth color-morph
Modes: Light, Dark, System
Storage: localStorage (persists user choice)
FILES TO CREATE / MODIFY:
+ src/styles/tokens-dark.css (dark variants of all tokens)
+ src/styles/dark-mode.css (transitions + toggle styles)
+ src/components/ThemeToggle/<files> (the toggle component)
+ src/hooks/useTheme.ts (state + system pref + storage)
± src/styles/tokens.css (adds [data-theme="dark"] selector)
± src/main.tsx / src/App.tsx (provider wired in)
± tailwind.config.ts (darkMode: 'class' if Tailwind)
± index.html (early <script> to avoid flash of light)
NOTHING runs until you type GO.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 4 — Generate Dark Variants
If tokens.css already exists, generate tokens-dark.css by inverting brightness while preserving hue:
Heuristic:
--color-bg: #FFFFFF → #0A0A0A (near-black, not pure)
--color-bg-subtle: #F8FAFC → #1A1A1F
--color-text: #0F172A → #F1F5F9
--color-text-muted: #475569 → #94A3B8
--color-primary: #2563EB → #3B82F6 (slightly brighter for contrast)
- Shadows lose opacity in dark mode (or use white-shadow for "glow")
Result:
[data-theme="dark"] {
--color-bg: #0A0A0A;
--color-bg-subtle: #1A1A1F;
--color-text: #F1F5F9;
--color-text-muted:#94A3B8;
--color-primary: #3B82F6;
--color-border: #2A2A2F;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.6);
}
[data-theme="invert"] {
filter: invert(1) hue-rotate(180deg);
}
[data-theme="invert"] img,
[data-theme="invert"] video {
filter: invert(1) hue-rotate(180deg);
}
[data-theme="read"] {
--color-bg: #F4ECD8;
--color-text: #5C4B37;
filter: contrast(0.92);
}
Step 5 — Generate the Toggle (Black or White)
Pick from templates/dark-mode/:
toggle-classic.tsx — simple sun/moon
toggle-black-or-white.tsx — the MJ tribute
The Black or White toggle uses CSS view-transitions API where supported, falls back to a clip-path circular reveal animation:
'use client'
import { useTheme } from '@/hooks/useTheme'
export function BlackOrWhiteToggle() {
const { theme, setTheme } = useTheme()
const toggle = async (e: React.MouseEvent) => {
const next = theme === 'dark' ? 'light' : 'dark'
if ('startViewTransition' in document) {
const x = e.clientX, y = e.clientY
const radius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(() => setTheme(next))
await transition.ready
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`,
],
},
{ duration: 600, easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)' },
)
} else {
setTheme(next)
}
}
return (
<button
aria-label="Toggle theme — Black or White"
onClick={toggle}
className="bow-toggle"
data-theme-state={theme}
>
<span className="bow-orb" />
<span className="sr-only">It don't matter if you're black or white</span>
</button>
)
}
CSS for the orb (lives in dark-mode.css):
.bow-toggle {
position: relative;
width: 56px;
height: 32px;
border-radius: 999px;
background: linear-gradient(90deg, #FFFFFF 0%, #FFFFFF 50%, #0A0A0A 50%, #0A0A0A 100%);
background-size: 200% 100%;
background-position: 0% 0%;
border: 1px solid var(--color-border);
cursor: pointer;
transition: background-position 600ms cubic-bezier(.4,0,.2,1);
}
[data-theme="dark"] .bow-toggle {
background-position: 100% 0%;
}
.bow-orb {
position: absolute;
top: 3px;
left: 3px;
width: 24px;
height: 24px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #1A1A1F 0%, #0A0A0A 70%);
box-shadow: 0 0 0 2px rgba(255,255,255,0.1), inset 0 0 8px rgba(255,255,255,0.2);
transition: transform 600ms cubic-bezier(.4,0,.2,1),
background 400ms ease;
}
[data-theme="dark"] .bow-orb {
transform: translateX(24px);
background: radial-gradient(circle at 65% 35%, #FFFFFF 0%, #E5E5E5 70%);
}
.bow-toggle::after {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 56 32'><text x='28' y='22' text-anchor='middle' font-size='10' fill='currentColor' opacity='0.15'>♪</text></svg>");
pointer-events: none;
opacity: 0;
transition: opacity 300ms ease;
}
.bow-toggle:hover::after { opacity: 0.4; }
:root, [data-theme] {
transition: background-color 400ms ease, color 400ms ease, border-color 400ms ease;
}
Step 6 — Hook + Provider
src/hooks/useTheme.ts
import { useEffect, useState } from 'react'
type Theme = 'light' | 'dark' | 'system'
export function useTheme() {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'system'
return (localStorage.getItem('theme') as Theme) || 'system'
})
const resolved = theme === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme
useEffect(() => {
document.documentElement.dataset.theme = resolved
}, [resolved])
useEffect(() => {
localStorage.setItem('theme', theme)
}, [theme])
useEffect(() => {
if (theme !== 'system') return
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const handler = () => {
document.documentElement.dataset.theme = mq.matches ? 'dark' : 'light'
}
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [theme])
return { theme: resolved, mode: theme, setTheme: setThemeState }
}
index.html early script (avoids flash of unthemed content)
Insert in <head> BEFORE the main bundle:
<script>
(function() {
try {
var t = localStorage.getItem('theme') || 'system';
var resolved = t === 'system'
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: t;
document.documentElement.dataset.theme = resolved;
} catch(e){}
})();
</script>
Step 7 — Tailwind Config Update (if applicable)
In tailwind.config.{js,ts}:
export default {
darkMode: ['class', '[data-theme="dark"]'],
}
Then dark:bg-bg-subtle etc. work everywhere automatically.
Step 8 — Final Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ DARK MODE LIVE — and it looks rad
The Black or White toggle is in src/components/ThemeToggle/.
Drop it into your nav: <BlackOrWhiteToggle />
WHAT YOU GET:
• Smooth circular-reveal animation on click
• System preference respected by default
• localStorage persists user choice
• No flash of light on page load (early script in index.html)
• Tailwind dark: variants work automatically [if Tailwind]
TRY IT:
npm run dev
→ click the toggle, watch the page flip ✨
🌟 If this looks slick and you want to share it:
The toggle is screenshot-bait. Tag #blackorwhite if you post it.
ADD MORE MODES?
Re-run /dark-mode and pick "Five-state" for Light/Dark/Invert/Read/System.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Pairs With
/design — generates the source tokens.css this skill inverts for dark mode
/scaffold and /webapp can offer dark mode as a post-step
Star-bait Notes
The MJ "Black or White" tribute is intentional — a memorable visual touch that makes the toggle feel like more than just CSS. The face-silhouette via SVG/text is subtle enough to be tasteful but easter-egg-y enough to make people screenshot it.