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:
colorsysuses HLS order (Hue, Lightness, Saturation), not HSL. Always unpack ash, l, s— noth, 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).