|
1 | 1 | import { useState } from "react"; |
2 | 2 |
|
3 | 3 | /** |
4 | | - * Resolve a theme CSS variable to its concrete color once on mount. |
5 | | - * framer-motion can't interpolate `var()` strings, so animated colors must be |
6 | | - * resolved to real values first. The fallback is used during SSR and should |
7 | | - * match the default dark theme (see tailwind.css). |
| 4 | + * Normalize any CSS color (hex, oklch, hsl, ...) to rgb()/rgba() by rendering |
| 5 | + * it to a 1x1 canvas. framer-motion can only interpolate hex/rgb/hsl, while |
| 6 | + * Tailwind v4's default palette is oklch. |
| 7 | + */ |
| 8 | +function toRgb(color: string): string { |
| 9 | + const canvas = document.createElement("canvas"); |
| 10 | + canvas.width = canvas.height = 1; |
| 11 | + const ctx = canvas.getContext("2d"); |
| 12 | + if (!ctx) return color; |
| 13 | + ctx.fillStyle = color; |
| 14 | + ctx.fillRect(0, 0, 1, 1); |
| 15 | + const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; |
| 16 | + return a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`; |
| 17 | +} |
| 18 | + |
| 19 | +/** |
| 20 | + * Resolve a theme CSS variable to a concrete, animatable color once on mount. |
| 21 | + * framer-motion can't interpolate `var()` strings or oklch values, so animated |
| 22 | + * colors must be resolved and normalized first. The fallback is used during |
| 23 | + * SSR and should match the default dark theme (see tailwind.css). |
8 | 24 | */ |
9 | 25 | export function useThemeColor(variable: `--${string}`, fallback: string): string { |
10 | 26 | const [color] = useState(() => { |
11 | 27 | if (typeof document === "undefined") return fallback; |
12 | | - return getComputedStyle(document.documentElement).getPropertyValue(variable).trim() || fallback; |
| 28 | + const value = getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); |
| 29 | + return value ? toRgb(value) : fallback; |
13 | 30 | }); |
14 | 31 | return color; |
15 | 32 | } |
0 commit comments