Color Format Conversion: Implementation of HEX, RGB, and HSL
Color Format Conversion: Implementation of HEX, RGB, and HSL#
As a frontend developer, color format conversion is routine work. Designers give you HEX from Figma, you want HSL for CSS variables, and Canvas needs RGB. Without understanding the principles, it’s easy to make mistakes.
Core Differences Between Color Models#
HEX: Compact Hexadecimal Notation#
HEX is essentially a hexadecimal shorthand for RGB:
#FF8800 → RGB(255, 136, 0)
Each pair of characters represents one color channel, ranging from 00 to FF (0-255). Case-insensitive, but #F80 (3-character shorthand) is parsed by browsers as #FF8800.
RGB: Additive Color Model#
Red, Green, Blue channels叠加, each 0-255. (0, 0, 0) is black, (255, 255, 255) is white.
HSL: More Intuitive Color Expression#
Hue, Saturation, Lightness. Compared to RGB, HSL aligns better with human color perception:
- H (Hue): 0-360 degrees, corresponding to position on the color wheel. 0° red, 120° green, 240° blue
- S (Saturation): 0%-100%, 0% is grayscale, 100% is pure color
- L (Lightness): 0%-100%, 0% black, 50% normal, 100% white
Want to “make the color brighter”? HSL: just change L. RGB: adjust all three channels simultaneously.
Conversion Algorithm Implementation#
HEX to RGB#
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
// Handle #F80 shorthand
const shorthand = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthand, (_, r, g, b) => r + r + g + g + b + b)
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null
}
hexToRgb('#FF8800') // { r: 255, g: 136, b: 0 }
hexToRgb('#F80') // { r: 255, g: 136, b: 0 }
RGB to HEX#
function rgbToHex(r: number, g: number, b: number): string {
const toHex = (n: number) => {
const hex = Math.max(0, Math.min(255, n)).toString(16)
return hex.length === 1 ? '0' + hex : hex
}
return '#' + toHex(r) + toHex(g) + toHex(b)
}
rgbToHex(255, 136, 0) // '#ff8800'
Note the boundary handling: RGB values must be clamped to 0-255, otherwise toString(16) may produce unexpected results.
RGB to HSL#
This is the most complex conversion:
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
r /= 255
g /= 255
b /= 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h = 0
let s = 0
const l = (max + min) / 2
if (max !== min) {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
break
case g:
h = ((b - r) / d + 2) / 6
break
case b:
h = ((r - g) / d + 4) / 6
break
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100),
}
}
rgbToHsl(255, 136, 0) // { h: 32, s: 100, l: 50 }
Key points:
- Hue calculation: First find the maximum channel, then calculate hue offset based on different channels
- Saturation formula: Depends on lightness value, different formulas for l > 50% and l < 50%
- Edge case: When
max === min, the color is grayscale, both hue and saturation are 0
HSL to RGB#
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
h /= 360
s /= 100
l /= 100
let r, g, b
if (s === 0) {
r = g = b = l // Grayscale
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1/6) return p + (q - p) * 6 * t
if (t < 1/2) return q
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
return p
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
}
}
hslToRgb(32, 100, 50) // { r: 255, g: 136, b: 0 }
The hue2rgb function handles the circular nature of the color wheel, ensuring color values transition smoothly within the correct range.
Practical Pitfalls#
Floating-Point Precision Issues#
RGB is integer, but HSL calculations involve extensive floating-point operations:
const rgb = { r: 100, g: 150, b: 200 }
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
const rgb2 = hslToRgb(hsl.h, hsl.s, hsl.l)
// rgb2 might be { r: 99, g: 150, b: 201 }, not exactly equal!
Round-trip conversion can have 1-2 units of error. This is normal. If you need exact matches, round after conversion.
Color Names to HEX Mapping#
CSS has 147 named colors that browsers convert internally:
document.querySelector('div').style.color = 'tomato'
getComputedStyle(document.querySelector('div')).color
// 'rgb(255, 99, 71)'
If you need color name to HEX conversion, maintain a mapping table. Recommend using css-color-names library.
Alpha Channel Handling#
RGBA and HSLA have additional transparency:
// HEX with alpha (8 digits)
function hexToRgba(hex: string): { r: number; g: number; b: number; a: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex)
if (!result) return null
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
a: result[4] ? parseInt(result[4], 16) / 255 : 1,
}
}
hexToRgba('#FF880080') // { r: 255, g: 136, b: 0, a: 0.5 }
Performance Optimization Tips#
Avoid Redundant Calculations#
Color picker dragging triggers conversion on every input event. Use useMemo for caching:
const hsl = useMemo(() => rgbToHsl(rgb.r, rgb.g, rgb.b), [rgb])
Pre-compute Common Colors#
const COLOR_CACHE = new Map<string, RGB>()
function cachedHexToRgb(hex: string): RGB | null {
if (COLOR_CACHE.has(hex)) {
return COLOR_CACHE.get(hex)!
}
const rgb = hexToRgb(hex)
if (rgb) COLOR_CACHE.set(hex, rgb)
return rgb
}
Use CSS Variables to Reduce JS Calculations#
For theme switching, prefer CSS variables over JS conversion:
:root {
--primary-h: 210;
--primary-s: 100%;
--primary-l: 50%;
}
.button {
background: hsl(var(--primary-h), var(--primary-s), var(--primary-l));
}
/* Dark mode just needs to change l value */
.dark {
--primary-l: 30%;
}
Online Tool Implementation#
Based on these principles, I built a Color Converter that supports:
- Real-time HEX / RGB / HSL conversion
- Native color picker integration
- One-click copy in any format
- Alpha channel support
The core code isn’t complex, but handling edge cases and precision issues requires attention to detail. Hope this helps!
Related tools: Color Contrast Checker | Color Palette Extractor