20 Dec, 2024
Theming with Tailwind CSS V4 Beta and Material Design 3
Get Theme
From Source Color
import { SchemeMonochrome, ... } from '@material/material-color-utilities';
export const constructors = {
[Variant.MONOCHROME]: SchemeMonochrome,
[Variant.NEUTRAL]: SchemeNeutral,
[Variant.TONAL_SPOT]: SchemeTonalSpot,
[Variant.VIBRANT]: SchemeVibrant,
[Variant.EXPRESSIVE]: SchemeExpressive,
[Variant.FIDELITY]: SchemeFidelity,
[Variant.CONTENT]: SchemeContent,
[Variant.RAINBOW]: SchemeRainbow,
[Variant.FRUIT_SALAD]: SchemeFruitSalad,
};
export const themeFromSourceColor = (
sourceColorArgb: number,
_variant: Variant = Variant.TONAL_SPOT,
_contrastLevel = 0.0
): Theme => {
const sourceColorHct = Hct.fromInt(sourceColorArgb);
const variant = _variant in Variant ? _variant : Variant.TONAL_SPOT;
const contrastLevel = clampDouble(-1.0, 1.0, _contrastLevel);
const scheme = new constructors[variant](
sourceColorHct,
false,
contrastLevel
);
const darkScheme = new constructors[variant](
sourceColorHct,
true,
contrastLevel
);
return {
source: sourceColorHct,
schemes: {
light: scheme,
dark: darkScheme,
},
palettes: {
primary: scheme.primaryPalette,
secondary: scheme.secondaryPalette,
tertiary: scheme.tertiaryPalette,
neutral: scheme.neutralPalette,
neutralVariant: scheme.neutralVariantPalette,
error: scheme.errorPalette,
},
};
};
From Image
export function sourceColorFromImageBytes(imageBytes: Uint8ClampedArray) {
// Convert Image data to Pixel Array
const pixels: number[] = [];
for (let i = 0; i < imageBytes.length; i += 4) {
const r = imageBytes[i];
const g = imageBytes[i + 1];
const b = imageBytes[i + 2];
const a = imageBytes[i + 3];
if (a < 255) {
continue;
}
const argb = argbFromRgb(r, g, b);
pixels.push(argb);
}
// Convert Pixels to Material Colors
const result = QuantizerCelebi.quantize(pixels, 128);
const ranked = Score.score(result);
const top = ranked[0];
return top;
}
const handleImageChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async () => {
sourceImage.value = reader.result as string;
const img = new Image();
img.src = sourceImage.value;
await new Promise((resolve) => (img.onload = resolve));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx?.drawImage(img, 0, 0);
const imageData = ctx?.getImageData(0, 0, img.width, img.height);
if (!imageData) return;
const argb = sourceColorFromImageBytes(imageData.data);
const color = hexFromArgb(argb);
if (color && color !== sourceColor.value) {
sourceColor.value = color;
reload();
}
};
reader.readAsDataURL(file);
}
};
Tailwind CSS Plugin
export default plugin.withOptions(
({ sourceColor = '#66b2b2' }: IOptions = {}) => {
const theme = themeFromSourceColor(argbFromHex(sourceColor));
return ({ addBase }) => {
addBase({
'@media (prefers-color-scheme: light)': {
':root': schemePropertiesToCssInJs(
getSchemeProperties(theme.schemes.light)
),
},
'@media (prefers-color-scheme: dark)': {
':root': schemePropertiesToCssInJs(
getSchemeProperties(theme.schemes.dark)
),
},
});
};
},
() => {
const roles = getColorRoles();
const colors = Object.fromEntries(
Object.entries(roles).map(([key]) => [key, `var(--md-sys-color-${key})`])
);
return {
theme: {
colors,
},
};
}
) as ReturnType<typeof plugin.withOptions>;
Inject Theme from Server Side
const theme = themeFromSourceColor(argbFromHex(sourceColor));
const getSchemeStyles = (properties: { [key: string]: number }) => {
return Object.entries(properties)
.map(([k, v]) => `${k}: ${hexFromArgb(v)};`)
.join('\n');
};
const lightSchemeStyles = getSchemeStyles(
getSchemeProperties(theme.schemes.light)
);
const darkSchemeStyles = getSchemeStyles(
getSchemeProperties(theme.schemes.dark)
);
const styleRaw = `
@layer theme, base, components, utilities, app;
@layer app {
@media (prefers-color-scheme: light) {
:root {
${lightSchemeStyles}
}
}
@media (prefers-color-scheme: dark) {
:root {
${darkSchemeStyles}
}
}
[data-scheme='light'] {
${lightSchemeStyles}
}
[data-scheme='dark'] {
${darkSchemeStyles}
}
}
`;