oklch is a great addition to css, providing much better control over color modifications, support for wider gamut, etc - reasons I'm not going to go into in this post.

But... sometimes you still might need, or want, good old hex colors. Email templates immediately come to mind.

So, if you happen to be collecting client branding colors in your application, for instance, and you're storing them as oklch instead of hex values, you'll need a way to convert to hex to inject the right value into your email template. Or whatever other use cases you can come up with.

If you're just doing one-off conversions, go ahead and visit oklch.com. But, if you need it dynamic, you might want some PHP code to do the conversion automatically for you.

I got you, fam:

<?php

/**
 * Convert OKLCH string to Hex
 * @param string $oklchString - OKLCH color string (e.g., "oklch(70% 0.15 30)" or "oklch(0.7 0.15 30deg)")
 * @return string Hex color string (e.g., "#ff5733")
 * @throws InvalidArgumentException
 */
function oklchToHex(string $oklchString): string
{
    // Parse OKLCH string
    if (!preg_match('/oklch\s*\(\s*([0-9.]+)%?\s+([0-9.]+)\s+([0-9.]+)(?:deg)?\s*\)/i', $oklchString, $match)) {
        throw new InvalidArgumentException('Invalid OKLCH string format');
    }
    
    $l = (float) $match[1];
    $c = (float) $match[2];
    $h = (float) $match[3];
    
    // Convert percentage lightness to 0-1 range
    if (str_contains($oklchString, '%')) {
        $l *= 0.01;
    }
    
    // Convert OKLCH to OKLAB
    $hRad = deg2rad($h);
    $a = $c * cos($hRad);
    $b = $c * sin($hRad);
    
    // Convert OKLAB to linear LMS (corrected matrix)
    $l_ = $l + 0.3963377774 * $a + 0.2158037573 * $b;
    $m_ = $l - 0.1055613458 * $a - 0.0638541728 * $b;
    $s_ = $l - 0.0894841775 * $a - 1.2914855480 * $b;
    
    $l3 = $l_ * $l_ * $l_;
    $m3 = $m_ * $m_ * $m_;
    $s3 = $s_ * $s_ * $s_;
    
    // Convert linear LMS to linear RGB (corrected matrix values)
    $r = 4.0767416621 * $l3 - 3.3077115913 * $m3 + 0.2309699292 * $s3;
    $g = -1.2684380046 * $l3 + 2.6097574011 * $m3 - 0.3413193965 * $s3;
    $b_rgb = -0.0041960863 * $l3 - 0.7034186147 * $m3 + 1.7076147010 * $s3;
    
    // Convert linear RGB to sRGB
    $r = linearToSrgb($r);
    $g = linearToSrgb($g);
    $b_rgb = linearToSrgb($b_rgb);
    
    // Clamp and convert to 8-bit
    $r = max(0, min(255, (int) round($r * 255)));
    $g = max(0, min(255, (int) round($g * 255)));
    $b_rgb = max(0, min(255, (int) round($b_rgb * 255)));
    
    // Convert to hex
    return sprintf('#%02x%02x%02x', $r, $g, $b_rgb);
}

/**
 * Convert linear RGB value to sRGB
 * @param float $val
 * @return float
 */
function linearToSrgb(float $val): float
{
    // Clamp to valid range first
    $val = max(0, min(1, $val));
    
    if ($val <= 0.0031308) {
        return 12.92 * $val;
    }
    return 1.055 * pow($val, 1 / 2.4) - 0.055;
}

// Example usage:
echo oklchToHex('oklch(70% 0.15 30)') . PHP_EOL;      // Warm orange
echo oklchToHex('oklch(0.5 0.2 200deg)') . PHP_EOL;   // Blue
echo oklchToHex('oklch(90% 0.05 120)') . PHP_EOL;     // Light green
echo oklchToHex('oklch(0.3 0.1 300deg)') . PHP_EOL;   // Dark purple

Prefer JS? No worries:

/**
 * Convert OKLCH string to Hex
 * @param {string} oklchString - OKLCH color string (e.g., "oklch(70% 0.15 30)" or "oklch(0.7 0.15 30deg)")
 * @returns {string} Hex color string (e.g., "#ff5733")
 */
function oklchToHex(oklchString) {
  // Parse OKLCH string
  const match = oklchString.match(/oklch\s*\(\s*([0-9.]+)%?\s+([0-9.]+)\s+([0-9.]+)(?:deg)?\s*\)/i);
  
  if (!match) {
    throw new Error('Invalid OKLCH string format');
  }
  
  let l = parseFloat(match[1]);
  let c = parseFloat(match[2]);
  let h = parseFloat(match[3]);
  
  // Convert percentage lightness to 0-1 range
  if (oklchString.includes('%')) {
    l = l / 100;
  }
  
  // Convert OKLCH to OKLAB
  const hRad = (h * Math.PI) / 180;
  const a = c * Math.cos(hRad);
  const b = c * Math.sin(hRad);
  
  // Convert OKLAB to linear LMS
  const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
  const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
  const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
  
  const l3 = l_ * l_ * l_;
  const m3 = m_ * m_ * m_;
  const s3 = s_ * s_ * s_;
  
  // Convert linear LMS to linear RGB
  let r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
  let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
  let b_rgb = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
  
  // Convert linear RGB to sRGB
  r = linearToSrgb(r);
  g = linearToSrgb(g);
  b_rgb = linearToSrgb(b_rgb);
  
  // Clamp and convert to 8-bit
  r = Math.max(0, Math.min(255, Math.round(r * 255)));
  g = Math.max(0, Math.min(255, Math.round(g * 255)));
  b_rgb = Math.max(0, Math.min(255, Math.round(b_rgb * 255)));
  
  // Convert to hex
  return '#' + [r, g, b_rgb].map(x => x.toString(16).padStart(2, '0')).join('');
}

function linearToSrgb(val) {
  // Clamp to valid range first
  val = Math.max(0, Math.min(1, val));
  
  if (val <= 0.0031308) {
    return 12.92 * val;
  }
  return 1.055 * Math.pow(val, 1 / 2.4) - 0.055;
}

// Example usage:
console.log(oklchToHex('oklch(70% 0.15 30)'));      // Warm orange
console.log(oklchToHex('oklch(0.5 0.2 200deg)'));   // Blue
console.log(oklchToHex('oklch(90% 0.05 120)'));     // Light green
console.log(oklchToHex('oklch(0.3 0.1 300deg)'));   // Dark purple

Happy color converting!