Skip to content
Back to skills

Dark Mode

ASecurity

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

  • 3 stars
  • 0 votes
  • 0 copies
  • 8 views
  • Added May 26, 2026
developmenttypescriptgobashreactvueapifrontend

Works with

  • cursor
  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add veekunth217/claude-scaffold-skill --skill dark-mode --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dark Mode?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dark Mode
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/veekunth217-dark-mode/badge)](https://www.skillsdirectory.com/skills/veekunth217-dark-mode)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

```bash
# Frontend framework
[ -f package.json ] && grep -E '"(react|vue|svelte|astro)"' package.json | head -3

# Existing theme
[ -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"

# Existing dark mode (don't overwrite)
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:

```css
/* tokens-dark.css */
[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:

```tsx
'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'

    // Modern browsers: smooth view transition with circular reveal
    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),
      )

      // @ts-ignore - new API
      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`):

```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%);
}

/* The face-morph easter egg — subtle silhouette overlay during transition */
.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; }

/* Smooth color transition for everything else when theme changes */
:root, [data-theme] {
  transition: background-color 400ms ease, color 400ms ease, border-color 400ms ease;
}
```

---

## Step 6 — Hook + Provider

### `src/hooks/useTheme.ts`

```typescript
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])

  // Listen for system pref changes if user is on 'system'
  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:

```html
<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}`:

```ts
export default {
  darkMode: ['class', '[data-theme="dark"]'],
  // ... rest unchanged
}
```

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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.