Toolmingo
Guides12 min read

How to Convert Between Color Formats (HEX, RGB, HSL, HSV, CMYK)

Learn how to convert between HEX, RGB, HSL, HSV, and CMYK color formats with working code examples in JavaScript, Python, Go, and PHP. Covers conversion formulas, pitfalls, and when to use each format.

How to Convert Between Color Formats

Web development uses multiple color formats: HEX for CSS shorthand, RGB for DOM manipulation, HSL for human-friendly adjustments, HSV for design tools, and CMYK for print. Converting between them requires understanding their underlying math, not just memorizing lookup tables.

This guide covers every major conversion with working implementations in JavaScript, Python, Go, and PHP.


Color Format Quick Reference

Format Example Range Best for
HEX #FF6B35 #000000#FFFFFF CSS, HTML, design tokens
RGB rgb(255, 107, 53) 0–255 per channel DOM manipulation, Canvas
HSL hsl(18, 100%, 60%) H: 0–360°, S/L: 0–100% Human-readable, theming
HSV / HSB hsv(18, 79%, 100%) H: 0–360°, S/V: 0–100% Photoshop, design tools
CMYK cmyk(0, 58, 79, 0) 0–100 per channel Print / offset printing

HEX ↔ RGB

HEX is just RGB written in base-16. Each pair of hex digits is one colour channel (red, green, blue), ranging from 00 (0) to FF (255).

#FF6B35
 ↑↑ ↑↑ ↑↑
 RR GG BB

JavaScript

function hexToRgb(hex) {
  const clean = hex.replace("#", "");
  // Expand 3-digit shorthand: #F60 → FF6600
  const full =
    clean.length === 3
      ? clean.split("").map((c) => c + c).join("")
      : clean;
  const n = parseInt(full, 16);
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}

function rgbToHex(r, g, b) {
  return (
    "#" +
    [r, g, b]
      .map((v) => Math.round(v).toString(16).padStart(2, "0"))
      .join("")
      .toUpperCase()
  );
}

hexToRgb("#FF6B35");   // { r: 255, g: 107, b: 53 }
rgbToHex(255, 107, 53); // "#FF6B35"

Python

def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
    h = hex_color.lstrip("#")
    if len(h) == 3:
        h = "".join(c * 2 for c in h)
    return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))

def rgb_to_hex(r: int, g: int, b: int) -> str:
    return "#{:02X}{:02X}{:02X}".format(r, g, b)

hex_to_rgb("#FF6B35")   # (255, 107, 53)
rgb_to_hex(255, 107, 53)  # "#FF6B35"

Go

import (
    "fmt"
    "strconv"
    "strings"
)

type RGB struct{ R, G, B uint8 }

func HexToRGB(hex string) (RGB, error) {
    h := strings.TrimPrefix(hex, "#")
    if len(h) == 3 {
        h = string([]byte{h[0], h[0], h[1], h[1], h[2], h[2]})
    }
    n, err := strconv.ParseInt(h, 16, 32)
    if err != nil {
        return RGB{}, err
    }
    return RGB{R: uint8(n >> 16), G: uint8(n >> 8), B: uint8(n)}, nil
}

func RGBToHex(c RGB) string {
    return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}

PHP

function hexToRgb(string $hex): array {
    $h = ltrim($hex, '#');
    if (strlen($h) === 3) {
        $h = $h[0].$h[0].$h[1].$h[1].$h[2].$h[2];
    }
    return [
        'r' => hexdec(substr($h, 0, 2)),
        'g' => hexdec(substr($h, 2, 2)),
        'b' => hexdec(substr($h, 4, 2)),
    ];
}

function rgbToHex(int $r, int $g, int $b): string {
    return '#' . strtoupper(sprintf('%02x%02x%02x', $r, $g, $b));
}

RGB ↔ HSL

HSL separates Hue (colour wheel angle), Saturation (colour intensity), and Lightness (brightness from black to white). It's far easier to create colour variations in HSL than in RGB.

Conversion formulas

RGB (0–255) → normalised rgb (0–1):
  r′ = R/255,  g′ = G/255,  b′ = B/255

Cmax = max(r′, g′, b′)
Cmin = min(r′, g′, b′)
Δ    = Cmax − Cmin

Lightness:  L = (Cmax + Cmin) / 2

Saturation:
  if Δ == 0 → S = 0
  else      → S = Δ / (1 − |2L − 1|)

Hue:
  if Δ == 0  → H = 0
  if Cmax=r′ → H = 60 × ((g′−b′)/Δ mod 6)
  if Cmax=g′ → H = 60 × ((b′−r′)/Δ + 2)
  if Cmax=b′ → H = 60 × ((r′−g′)/Δ + 4)

JavaScript

function rgbToHsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  const delta = max - min;
  let h = 0, s = 0;
  const l = (max + min) / 2;

  if (delta !== 0) {
    s = delta / (1 - Math.abs(2 * l - 1));
    switch (max) {
      case r: h = ((g - b) / delta + 6) % 6; break;
      case g: h = (b - r) / delta + 2;        break;
      case b: h = (r - g) / delta + 4;        break;
    }
    h = Math.round(h * 60);
  }
  return { h, s: Math.round(s * 100), l: Math.round(l * 100) };
}

function hslToRgb(h, s, l) {
  s /= 100; l /= 100;
  const a = s * Math.min(l, 1 - l);
  const f = (n) => {
    const k = (n + h / 30) % 12;
    return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
  };
  return {
    r: Math.round(f(0) * 255),
    g: Math.round(f(8) * 255),
    b: Math.round(f(4) * 255),
  };
}

rgbToHsl(255, 107, 53);   // { h: 18, s: 100, l: 60 }
hslToRgb(18, 100, 60);    // { r: 255, g: 107, b: 54 }  (rounding ±1)

Python

import colorsys

def rgb_to_hsl(r: int, g: int, b: int) -> tuple[int, int, int]:
    # colorsys uses HLS order (hue, lightness, saturation)
    h, l, s = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
    return round(h * 360), round(s * 100), round(l * 100)

def hsl_to_rgb(h: int, s: int, l: int) -> tuple[int, int, int]:
    r, g, b = colorsys.hls_to_rgb(h / 360, l / 100, s / 100)
    return round(r * 255), round(g * 255), round(b * 255)

rgb_to_hsl(255, 107, 53)  # (18, 100, 60)
hsl_to_rgb(18, 100, 60)   # (255, 107, 54)

Python pitfall: colorsys uses HLS order (Hue, Lightness, Saturation), not HSL. Always unpack as h, l, s — not h, s, l.


RGB ↔ HSV (HSB)

HSV (Hue, Saturation, Value) is also called HSB (Brightness). It's the model used in Photoshop's colour picker and many design tools. Unlike HSL where L=100% is white regardless of saturation, in HSV V=100% with S=0% gives pure white, and V=0% is always black.

Normalise: r′=R/255, g′=G/255, b′=B/255
Cmax = max(r′,g′,b′)
Δ    = Cmax − min(r′,g′,b′)

Value:      V = Cmax
Saturation: S = 0 if Cmax==0, else Δ/Cmax
Hue:        same formula as HSL

JavaScript

function rgbToHsv(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  const delta = max - min;
  let h = 0;
  if (delta !== 0) {
    switch (max) {
      case r: h = ((g - b) / delta + 6) % 6; break;
      case g: h = (b - r) / delta + 2;        break;
      case b: h = (r - g) / delta + 4;        break;
    }
    h = Math.round(h * 60);
  }
  return {
    h,
    s: Math.round(max === 0 ? 0 : (delta / max) * 100),
    v: Math.round(max * 100),
  };
}

function hsvToRgb(h, s, v) {
  s /= 100; v /= 100;
  const f = (n) => {
    const k = (n + h / 60) % 6;
    return v - v * s * Math.max(0, Math.min(k, 4 - k, 1));
  };
  return {
    r: Math.round(f(5) * 255),
    g: Math.round(f(3) * 255),
    b: Math.round(f(1) * 255),
  };
}

rgbToHsv(255, 107, 53); // { h: 18, s: 79, v: 100 }
hsvToRgb(18, 79, 100);  // { r: 255, g: 107, b: 53 }

Python

import colorsys

def rgb_to_hsv(r: int, g: int, b: int) -> tuple[int, int, int]:
    h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
    return round(h * 360), round(s * 100), round(v * 100)

def hsv_to_rgb(h: int, s: int, v: int) -> tuple[int, int, int]:
    r, g, b = colorsys.hsv_to_rgb(h / 360, s / 100, v / 100)
    return round(r * 255), round(g * 255), round(b * 255)

rgb_to_hsv(255, 107, 53) # (18, 79, 100)
hsv_to_rgb(18, 79, 100)  # (255, 107, 53)

RGB ↔ CMYK

CMYK (Cyan, Magenta, Yellow, Key/Black) is a subtractive colour model used in print. Unlike RGB (additive — mixing light), CMYK describes ink percentages applied on white paper.

Important: There is no universal RGB↔CMYK mapping. The conversion below is a mathematical approximation. Professional print work requires ICC colour profiles for accurate conversion.

Formula

Normalise: r′=R/255, g′=G/255, b′=B/255
K = 1 − max(r′, g′, b′)

if K == 1: C=M=Y=0 (pure black)
else:
  C = (1 − r′ − K) / (1 − K)
  M = (1 − g′ − K) / (1 − K)
  Y = (1 − b′ − K) / (1 − K)

Multiply C, M, Y, K by 100 for percentage values.

JavaScript

function rgbToCmyk(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const k = 1 - Math.max(r, g, b);
  if (k === 1) return { c: 0, m: 0, y: 0, k: 100 };
  const d = 1 - k;
  return {
    c: Math.round(((1 - r - k) / d) * 100),
    m: Math.round(((1 - g - k) / d) * 100),
    y: Math.round(((1 - b - k) / d) * 100),
    k: Math.round(k * 100),
  };
}

function cmykToRgb(c, m, y, k) {
  c /= 100; m /= 100; y /= 100; k /= 100;
  return {
    r: Math.round(255 * (1 - c) * (1 - k)),
    g: Math.round(255 * (1 - m) * (1 - k)),
    b: Math.round(255 * (1 - y) * (1 - k)),
  };
}

rgbToCmyk(255, 107, 53); // { c: 0, m: 58, y: 79, k: 0 }
cmykToRgb(0, 58, 79, 0); // { r: 255, g: 107, b: 53 }

Python

def rgb_to_cmyk(r: int, g: int, b: int) -> tuple[int, int, int, int]:
    r_, g_, b_ = r / 255, g / 255, b / 255
    k = 1 - max(r_, g_, b_)
    if k == 1:
        return 0, 0, 0, 100
    d = 1 - k
    return (
        round((1 - r_ - k) / d * 100),
        round((1 - g_ - k) / d * 100),
        round((1 - b_ - k) / d * 100),
        round(k * 100),
    )

def cmyk_to_rgb(c: int, m: int, y: int, k: int) -> tuple[int, int, int]:
    c_, m_, y_, k_ = c / 100, m / 100, y / 100, k / 100
    return (
        round(255 * (1 - c_) * (1 - k_)),
        round(255 * (1 - m_) * (1 - k_)),
        round(255 * (1 - y_) * (1 - k_)),
    )

rgb_to_cmyk(255, 107, 53)  # (0, 58, 79, 0)
cmyk_to_rgb(0, 58, 79, 0)  # (255, 107, 53)

Full Round-Trip: HEX → RGB → HSL

Building on the individual converters above, a complete round-trip is straightforward:

// JavaScript — full chain
function hexToHsl(hex) {
  const { r, g, b } = hexToRgb(hex);
  return rgbToHsl(r, g, b);
}

function hslToHex(h, s, l) {
  const { r, g, b } = hslToRgb(h, s, l);
  return rgbToHex(r, g, b);
}

hexToHsl("#FF6B35");   // { h: 18, s: 100, l: 60 }
hslToHex(18, 100, 60); // "#FF6B35" (or very close due to rounding)

Quick Reference: Conversion Paths

From → To Direct? Via
HEX → RGB Base-16 parse
RGB → HEX toString(16)
RGB → HSL Math formula
HSL → RGB Math formula
RGB → HSV Math formula
HSV → RGB Math formula
RGB → CMYK Math formula
CMYK → RGB Math formula
HEX → HSL 🔄 HEX → RGB → HSL
HSL → HEX 🔄 HSL → RGB → HEX
HSV → HSL 🔄 HSV → RGB → HSL
CMYK → HEX 🔄 CMYK → RGB → HEX

RGB is the universal hub — all conversions pass through it.


6 Common Mistakes

1. Forgetting padStart(2, "0") in hex output

// WRONG — single-digit values become 1 hex char
(5).toString(16) // "5" — should be "05"

// CORRECT
(5).toString(16).padStart(2, "0") // "05"

2. Python's colorsys uses HLS order, not HSL

# WRONG
h, s, l = colorsys.rgb_to_hls(r, g, b)  # s and l are swapped!

# CORRECT
h, l, s = colorsys.rgb_to_hls(r, g, b)

The function is named rgb_to_hls and returns hue, lightness, saturation in that order.

3. Not normalising RGB input

All HSL/HSV formulas expect values in the 0–1 range, not 0–255. Forgetting to divide gives wildly wrong hue angles.

// WRONG
const max = Math.max(r, g, b); // r=255 → max=255, not 1.0

// CORRECT
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b); // max ≤ 1.0

4. Treating CMYK conversion as colour-accurate

Mathematical RGB↔CMYK is an approximation. A logo that looks right on screen may print significantly different because:

  • Different paper absorbs ink differently
  • Black ink (K) replaces RGB grey components differently per printer
  • Pantone/spot colours have no RGB equivalent

For print work, use ICC profiles and a colour management system.

5. Not handling hue wrap-around

Hue is a circular value (0–360°). After arithmetic, always normalise:

// Adding 30° to hue 350° should give 20°, not 380°
const newHue = (hue + 30) % 360;

6. Confusing HSV and HSL

Both have Hue and Saturation, but they are not the same:

Pure red HSL HSV
rgb(255,0,0) hsl(0,100%,50%) hsv(0,100%,100%)
White rgb(255,255,255) hsl(0,0%,100%) hsv(0,0%,100%)
  • HSL L=100% is always white regardless of saturation.
  • HSV V=100%, S=0% is white; V=0% is always black.

Frequently Asked Questions

Which format should I use in CSS? Use hsl() for colour theming and variations (easy to lighten/darken), #RRGGBB hex for design tokens and static values, and rgb() when you need to compose colours dynamically in JavaScript.

Can I convert HEX to CMYK directly? Not in one step. Parse HEX to RGB first, then apply the CMYK formula. See the Quick Reference table above — RGB is the universal hub.

Why does round-tripping HEX → HSL → HEX sometimes lose precision? HSL stores hue as an integer degree (0–360) and saturation/lightness as integer percentages (0–100), which introduces ±1 rounding errors. These errors are invisible to the eye but show up in bit-exact comparisons. Use floating-point intermediate values if precision matters.

What is the 8-digit hex format (#RRGGBBAA)? The last two digits are the alpha channel (opacity), from 00 (fully transparent) to FF (fully opaque). #FF6B3580 is the same orange at 50% opacity. Modern browsers support it in CSS.

Does Python have a built-in CMYK converter? No. The colorsys module covers RGB, HSV, HSL (HLS), and YIQ only. Use the manual formula shown above, or a third-party library like colormath for ICC-profile-aware conversions.

How accurate is the mathematical RGB→CMYK formula for printing? It's device-independent and purely mathematical — useful for approximation and for building colour tools, but not for professional print. For accurate printing, embed an ICC profile and use a colour management system (Photoshop, GIMP with colour management enabled, or the lcms2 C library via Python Pillow).

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools