🎨

OKLCHで配色設計してみた:React/Tailwindなら明るさを崩さずに

に公開

はじめに

最近、業務でマークアップ(React/TypeScript)を触ることが増えて、配色の崩れやすさに何度か泣きました。そこで OKLCH を軸に「明るさ(L)と彩度(C)を意識したトークン設計」を試したらかなり楽になったのでメモします。

結論、OKLCH + CSS変数 + Tailwindの組み合わせだと、ライト/ダークどちらでも破綻しにくい配色をトークン化できて、Hover/Active みたいな状態色も CSSだけでほぼ自動生成できます。

なんでOKLCH?

  • 人間の見え方に近い軸(L/C/H)で色を決められる
    → 背景と前景のコントラスト調整がやりやすい。
  • ライト/ダークの切り替えが安定
    → H(色相)はなるべく固定、L/Cだけ動かすのがコツ。
  • CSSだけで派生色が作れる
    → color-mix() で Hover/Press を算出。JSに寄せなくてOK。

ざっくり方針

  1. CSS変数でトークン宣言(--brand / --bg / --fg / --surface など)
  2. Tailwindの色にブリッジ(oklch(var(--brand)/<alpha-value>) みたいに)
  3. 派生色はCSSで生成(--brand-hover 等を color-mix() で)
  4. ダークはL/Cだけ再調整(Hは固定してブランド感を保つ)

実装スニペット

globals.css

:root {
  --brand: oklch(0.62 0.19 265);
  --bg:    oklch(0.98 0.01 255);
  --fg:    oklch(0.18 0.03 255);

  --brand-hover: color-mix(in oklch, var(--brand), white 12%);
  --brand-press: color-mix(in oklch, var(--brand), black 10%);
  --surface:     oklch(0.98 0.01 255);
  --surface-2:   color-mix(in oklch, var(--surface), black 4%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:    oklch(0.15 0.02 255);
    --fg:    oklch(0.93 0.02 255);
    --brand: oklch(0.70 0.17 265);
    --surface:   oklch(0.20 0.02 255);
    --surface-2: color-mix(in oklch, var(--surface), white 6%);
    --brand-hover: color-mix(in oklch, var(--brand), white 10%);
    --brand-press: color-mix(in oklch, var(--brand), black 12%);
  }
}

tailwind.config.ts

import type { Config } from "tailwindcss";

export default {
  content: ["./src/**/*.{ts,tsx,mdx}"],
  theme: {
    extend: {
      colors: {
        bg: "oklch(var(--bg) / <alpha-value>)",
        fg: "oklch(var(--fg) / <alpha-value>)",
        brand: "oklch(var(--brand) / <alpha-value>)",
        surface: "oklch(var(--surface) / <alpha-value>)",
        "surface-2": "oklch(var(--surface-2) / <alpha-value>)",
      },
      ringColor: { DEFAULT: "oklch(var(--brand) / 1)" },
    },
  },
} satisfies Config;

Button

type ButtonProps = React.ComponentProps<"button"> & { variant?: "primary" | "ghost" };

export function Button({ variant = "primary", className = "", ...props }: ButtonProps) {
  const base =
    variant === "primary"
      ? "bg-brand text-white hover:bg-[--brand-hover] active:bg-[--brand-press] focus-visible:ring"
      : "bg-surface-2 text-fg hover:bg-[color-mix(in_oklch,var(--surface-2),white_6%)]";

  return (
    <button
      className={`inline-flex items-center rounded-xl px-4 py-2 transition ${base} ${className}`}
      {...props}
    />
  );
}

まとめ

OKLCHをベースのトークン→ユーティリティの流れを作ると、配色レビューの迷いが激減しました。React/TSでも型でバリアントを整理しやすくて、UIの追加にも強いです。
同じように配色で悩んでる人は、ぜひ手元のプロジェクトで試してみてください!

Discussion