ホーム / Skills / Web・UIデザイン / ライト/ダークテーマ(Light and Dark Theme)
Web・UIデザイン

ライト/ダークテーマ(Light and Dark Theme)

AIBarsMITSkill の言語: 英語
ライセンス確認済みスクリプトなし低リスク
デモ実行記録 · claude-sonnet-5-5 · 2026年10月
Using the light-dark-theme skill, plan how to add light and dark mode to a small server-rendered Nuxt 3 app that uses plain CSS. Today its colors are hard-coded hex values in components. Give the token approach, the root and head setup, the System/Light/Dark toggle, how to avoid the flash of the wrong theme and the hydration mismatch, and how you would verify it. You cannot run the app, so say what you could not check.
Skill: light-dark-theme
Read: references/token-starter.md

Plan: light and dark mode for the Nuxt 3 app (plain CSS, SSR)

I haven't looked at your code, so file names below are placeholders. I did not run anything.

1. Token approach

  1. Audit. Grep components for #[0-9a-fA-F]{3,8}, rgb(, hsl( and named colors, including inline style and SVG fill/stroke attributes. Group the values by role. A handful of distinct grays usually collapses into 3–4 surface and text roles.
  2. Define role tokens, never value-named ones, in assets/css/theme.css. Register it in nuxt.config.ts under css: ['~/assets/css/theme.css']. Role tokens:
    • Surfaces: --bg, --surface, --surface-raised, --surface-input.
    • Text: --text, --text-secondary, --text-disabled, --text-inverse, --link.
    • Borders and focus: --border, --border-strong, --focus-ring.
    • Brand: --primary, --primary-hover, --on-primary.
    • Status: --success, --warning, --danger, --info, each with an --on-* token.
    • --shadow.
  3. Pick values. Seed from the skill's starter palette, then substitute your brand colors. Set each dark value deliberately, with no filter: invert and no mechanical lightness flip. Re-tune the status colors per theme.
  4. Migrate components. Replace every hex with var(--token). No .dark .card {} overrides. Switch SVG icons to currentColor.

2. Root and head setup

CSS, using data-theme on <html> as the single switch:

:root { color-scheme: light; /* light tokens */ }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark; /* dark tokens */ }
}
:root[data-theme="dark"] { color-scheme: dark; /* dark tokens */ }

The dark tokens appear twice. To keep the copies from drifting, I'd generate one from the other with a small build step (PostCSS or a script). If that is too heavy, I'd put a comment on both blocks and add a check that diffs them.

Head setup in nuxt.config.ts:

app: {
  head: {
    meta: [
      { name: 'color-scheme', content: 'light dark' },
      { name: 'theme-color', content: '#ffffff', media: '(prefers-color-scheme: light)' },
      { name: 'theme-color', content: '#0f1115', media: '(prefers-color-scheme: dark)' },
    ],
    script: [{
      innerHTML: `(function(){try{var s=localStorage.getItem('theme');if(s==='light'||s==='dark')document.documentElement.setAttribute('data-theme',s)}catch(e){}})()`,
      tagPosition: 'head',
    }],
  },
},

Nuxt/unhead renders a script with innerHTML as an inline, synchronous <script> in the server HTML. I'd check the rendered HTML to confirm it has no defer, async or type="module". Also check that the project's CSP, if it has one, allows inline scripts, with a nonce or hash if needed. Without a stored choice the script does nothing, so the media query handles System.

3. System / Light / Dark toggle

A useTheme() composable plus a ThemeToggle.vue component:

  • State: choice is 'system' | 'light' | 'dark'.
  • Select Light or Dark: set data-theme and write localStorage.theme.
  • Select System: remove the attribute and remove the key. Never store the word "system".
  • Storage access: wrap every read and write in try/catch.
  • System changes: while the choice is System, listen to matchMedia('(prefers-color-scheme: dark)') changes. This matters mainly for theme-color and for any JS that reads the theme. CSS follows the media query on its own.
  • Markup: a three-option radio group (<fieldset> with role="radiogroup") or three buttons with aria-pressed. It shows the current choice and has an accessible name.

4. Avoiding the flash and the hydration mismatch

Flash.

  • The inline head script sets data-theme before first paint.
  • <meta name="color-scheme"> plus color-scheme in CSS prevents a white canvas.
  • No theme transitions on first paint. Add a theme-ready class after mount if you want them.

Hydration mismatch. The server can't know the saved choice, so the server HTML must not depend on it.

  • Initialize choice to 'system' identically on server and client, and read storage only in onMounted.
  • Don't use useState or useCookie to render a different icon or label per theme.
  • Make the toggle's visual state not depend on JS state at first render. Two options: render the control's checked state only after mount, or drive it from CSS with :root[data-theme="dark"] .opt-dark { … }.
  • Don't bind data-theme through Vue on <html>. The head script owns it, and a Vue-managed attribute can conflict with it on hydration.
  • Never branch on import.meta.client or window during render.

5. Other items to handle

  • Logos and images with baked-in backgrounds: use <picture> with a media query, or a variant switched by data-theme. Note that <picture> follows the system setting, not the manual choice, so for a manual override use two <img> elements toggled by CSS.
  • Shadows, selection color, scrollbars, code blocks, charts, and third-party embeds. Embeds may remain known gaps.
  • Print styles and email templates should stay light, set explicitly.

6. Verification

Contrast, checked per theme and per state (hover, focus, disabled, error, placeholder, link):

  • --text on --bg, --surface and --surface-raised.
  • --text-secondary on --bg and --surface.
  • --link on --bg and --surface.
  • --on-primary on --primary and --primary-hover.
  • Each --on-<status> on its status color.
  • --border-strong and --focus-ring against their surfaces (3:1).
  • Placeholder on --surface-input.

I'd compute these ratios with a script from the token values, which needs no browser. Thresholds are 4.5:1 for text and 3:1 for large text and UI parts.

Browser matrix, at phone and desktop widths:

  • First load with no stored choice (system light and system dark), stored light, and stored dark. Throttle CPU and cache to catch any flash.
  • Hard reload and client-side navigation.
  • Toggle persists across reloads. System follows the OS.
  • Storage blocked (private window or blocked site data): the page still renders correctly.
  • Console shows no hydration warning.
  • Keyboard focus rings visible in both themes. No text, icon or border disappears.
  • curl the SSR HTML to confirm the inline script is in <head> and the toggle markup has no theme-dependent content.
  • Grep for leftover hard-coded colors.

What I could not check

I can't run the app, so none of the following is verified:

  • Any visual result in either theme.
  • The actual flash behavior.
  • Hydration warnings.
  • Real contrast of your brand colors. Starter values are placeholders, and the ratios have to be re-checked once your colors are in.
  • Behavior at phone and desktop widths.
  • Toggle persistence and OS-follow behavior.
  • Blocked-storage behavior.
  • Which unhead version and CSP settings your app has, which affects the inline script.
  • Whether your images, charts or embeds have theme problems, because I haven't seen them.

Real run in an isolated folder with only this skill installed. Only the Skill and Read tools were enabled, so no command was run and no file was written. The scenario is fictional and the model had no access to the code. The model opened the skill's token starter file.

できること

Web UI にライト/ダークモードを追加・修正するための段階的な方法です。まずプロジェクトのフレームワークと既存のテーマの仕組みを確認し、16 進数の色を直接書く代わりに、役割ごとの意味トークン(背景、文字、枠線、ブランド色と状態色、およびその上に載せる文字色)を定義します。ルート要素の data-theme 属性 1 つでテーマを切り替え、既定ではシステム設定に従い、color-scheme を必ず設定してネイティブのコントロールやスクロールバーも合わせます。head に置く小さな同期スクリプトで誤ったテーマの一瞬の表示を防ぎ、保存された選択の読み取りは try/catch で囲み、サーバー側レンダリングでのハイドレーションの不一致を避ける方法も説明します。切替は「システム/ライト/ダーク」の 3 状態です。

チェックと修正

各テーマ・各状態でのコントラストの確認方法(通常の文字は 4.5:1、大きな文字や意味のある UI 部品は 3:1)と、よくある原因を挙げます。直書きの色、白背景の画像、currentColor を使っていないアイコン、影、グラフ、サードパーティの埋め込み、コードブロック、印刷スタイルです。参考ファイルには出発点となるトークン一式があり、その仮の色は、自身が挙げるコントラストの組み合わせで確認済みです。

向いている場面

既存サイトへのダークモードの追加、見た目のおかしいダークテーマの修正。

補足とリスク

低リスク:スクリプトのない指示のみのパッケージで、ネットワーク接続やファイル書き込みはありません。CSS と短いインラインスクリプトを提示するので、ご自身のプロジェクトに追加する前に確認してください。出発点の色は仮のものです。Skill が挙げるコントラストの組み合わせで確認済みですが、ブランド色に置き換えたら必ず再確認が必要です。動作中のページは見られず、できなかった確認は明示します。新しい CSS や meta の機能はブラウザによって対応が異なるため、対象ブラウザで確認してください。AIBars 制作のオリジナル(MIT)。架空の Nuxt アプリで 1 回試用しました。