Skip to content

Instantly share code, notes, and snippets.

@kseikyo
Last active May 30, 2025 01:36
Show Gist options
  • Save kseikyo/a3be0194eab64f0f0253a670f95d0961 to your computer and use it in GitHub Desktop.
Save kseikyo/a3be0194eab64f0f0253a670f95d0961 to your computer and use it in GitHub Desktop.
Migrate from shadcn ui HSL custom config to OKLCH
// file created to migrate from old shadcn hsl config to tailwind v4
// scripts/convert-colors-to-oklch.js
import fs from 'fs';
import convert from 'color-convert';
import path from 'path';
// Paths are relative to where the script is run (process.cwd())
const inputFilePathArg = process.argv[2];
const outputFilePathArg = process.argv[3];
if (!inputFilePathArg || !outputFilePathArg) {
console.error('Usage: node scripts/convert-colors-to-oklch.js <inputFile> <outputFile>');
console.error('Example: node scripts/convert-colors-to-oklch.js src/styles/input.css src/styles/output.css');
process.exit(1);
}
const absoluteInputPath = path.resolve(inputFilePathArg);
const absoluteOutputPath = path.resolve(outputFilePathArg);
try {
const cssContent = fs.readFileSync(absoluteInputPath, 'utf8');
const lines = cssContent.split('\n');
const newLines = [];
// Regex for HSL-like values: --name: H S% L% [/ A%]; /* comment */
// Example: --black: 230 40% 3%; /* #05060a */
const hslRegex = /^(\s*--[a-zA-Z0-9-]+:\s*)(\d{1,3})\s+(\d{1,3})%\s+(\d{1,3})%(?:\s*\/\s*([\d.]+%))?(\s*;.*)$/;
// Regex for HEX values: --name: #RGB or #RRGGBB or #RGBA or #RRGGBBAA; /* comment */
// Example: --blue-900-hex: #2775ca;
const hexRegex = /^(\s*--[a-zA-Z0-9-]+:\s*)(#[0-9a-fA-F]{3,8})(\s*;.*)$/;
for (const line of lines) {
let newLine = line;
const hslMatch = line.match(hslRegex);
const hexMatch = line.match(hexRegex);
if (hslMatch) {
const prefix = hslMatch[1];
const h = parseInt(hslMatch[2], 10);
const s = parseInt(hslMatch[3], 10);
const l = parseInt(hslMatch[4], 10);
const alphaPercentString = hslMatch[5]; // e.g., "50%" or undefined
try {
let convertedColor, colorModelName;
if (convert.hsl.oklch) {
convertedColor = convert.hsl.oklch([h, s, l]);
colorModelName = 'oklch';
} else if (convert.hsl.lch) {
console.warn("Oklch direct conversion not found for HSL, falling back to LCH for line: " + line);
convertedColor = convert.hsl.lch([h, s, l]);
colorModelName = 'lch';
} else {
throw new Error('Neither Oklch nor LCH conversion available for HSL.');
}
// convertedColor = [L, C, H]
// L: 0-100, C: 0-~130 (LCH) or 0-~0.4 (Oklch CSS target), H: 0-360
// CSS: oklch(L% C H [/ A]) or lch(L% C H [/ A])
const lCss = convertedColor[0].toFixed(2);
// For Oklch, C is typically 0 to ~0.4. color-convert's oklch C scale is 0-32.
// For LCH, C is typically 0 to ~130+. color-convert's lch C scale is 0-133.
// We'll scale Oklch's C by /100 for CSS. LCH's C can be used more directly.
const cCss = colorModelName === 'oklch' ? (convertedColor[1] / 100).toFixed(4) : convertedColor[1].toFixed(2);
const hCss = convertedColor[2].toFixed(2);
let colorString = `${colorModelName}(${lCss}% ${cCss} ${hCss}`;
if (alphaPercentString) {
colorString += ` / ${alphaPercentString}`;
}
colorString += `)`;
const suffix = hslMatch[6];
newLine = `${prefix}${colorString}${suffix}`;
} catch (e) {
console.warn(`Could not convert HSL value on line: ${line}. Error: ${e.message}`);
}
} else if (hexMatch) {
const prefix = hexMatch[1];
const hexColorWithHash = hexMatch[2]; // e.g., "#2775ca"
const suffix = hexMatch[3];
try {
const hexValue = hexColorWithHash.substring(1); // Remove #
// Determine if there's an alpha component in the hex string
let rgb, alphaFromHex = null;
if (hexValue.length === 4 || hexValue.length === 8) { // #RGBA or #RRGGBBAA
const alphaHex = hexValue.length === 4 ? hexValue.substring(3,4).repeat(2) : hexValue.substring(6,8);
alphaFromHex = parseInt(alphaHex, 16) / 255;
const colorHex = hexValue.length === 4 ? hexValue.substring(0,3) : hexValue.substring(0,6);
rgb = convert.hex.rgb(colorHex);
} else { // #RGB or #RRGGBB
rgb = convert.hex.rgb(hexValue);
}
let convertedColor, colorModelName;
if (convert.rgb.oklch) {
convertedColor = convert.rgb.oklch(rgb);
colorModelName = 'oklch';
} else if (convert.rgb.lch) {
console.warn("Oklch direct conversion not found for RGB, falling back to LCH for line: " + line);
convertedColor = convert.rgb.lch(rgb);
colorModelName = 'lch';
} else {
throw new Error('Neither Oklch nor LCH conversion available for RGB.');
}
const lCss = convertedColor[0].toFixed(2);
const cCss = colorModelName === 'oklch' ? (convertedColor[1] / 100).toFixed(4) : convertedColor[1].toFixed(2);
const hCss = convertedColor[2].toFixed(2);
let colorString = `${colorModelName}(${lCss}% ${cCss} ${hCss}`;
if (alphaFromHex !== null && alphaFromHex < 1.0) {
colorString += ` / ${(alphaFromHex * 100).toFixed(0)}%`;
}
colorString += `)`;
newLine = `${prefix}${colorString}${suffix}`;
} catch (e) {
console.warn(`Could not convert HEX value on line: ${line}. Error: ${e.message}`);
}
}
newLines.push(newLine);
}
fs.writeFileSync(absoluteOutputPath, newLines.join('\n'), 'utf8');
console.log(`Successfully converted colors. Output written to ${absoluteOutputPath}`);
} catch (error) {
console.error(`Error processing file: ${error.message}`);
process.exit(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment