Color Picker: Choose & Convert Colors (HEX, RGB, HSL)
Here is a widely held assumption worth examining: most developers default to HEX colors because that is what design tools export and what CSS tutorials show. But HEX is actually the least useful format once you need to programmatically adjust a color — darken a button on hover, generate a palette from a brand color, or check if two colors meet WCAG contrast requirements. You cannot do any of that by staring at #3B82F6.
This guide covers what each color format actually represents, the conversion math between them, when each format earns its place in production code, and why OKLCH — supported by 93%+ of browsers as of 2026, per Can I Use data — is the format most design systems should be adopting now. Use the BytePane Color Converter alongside this guide to see every conversion live.
Key Takeaways
- ▸HEX is compact notation for RGB — the two are mathematically identical. Use HEX for hardcoded static values in CSS.
- ▸HSL is the right format for palette generation — increment lightness to create shades, rotate hue to create complementary colors.
- ▸OKLCH solves HSL's perceptual uniformity problem — yellow and blue at the same OKLCH lightness actually look equally bright.
- ▸WCAG AA requires 4.5:1 contrast for normal text, 3:1 for large text — your color picker should verify this automatically.
- ▸CSS custom properties + HSL/OKLCH are the correct foundation for a themeable design system.
HEX: The Compact RGB Shorthand
A HEX color like #FF5733 is six hexadecimal digits representing three 8-bit channels: red, green, blue. Each pair encodes a value from 00 (0) to FF (255). There is no mathematical difference between HEX and RGB — they are the same color model with different notation.
/* 6-digit HEX: #RRGGBB */
color: #FF5733; /* red=255, green=87, blue=51 */
/* 3-digit shorthand: each digit doubles */
color: #F53; /* equivalent to #FF5533 */
/* 8-digit HEX with alpha: #RRGGBBAA */
color: #FF573380; /* 50% opacity (80 hex = 128 dec, 128/255 ≈ 0.5) */
/* 4-digit shorthand with alpha */
color: #F538; /* equivalent to #FF553388 */HEX ↔ RGB Conversion
// HEX to RGB in JavaScript
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-fd]{2})([a-fd]{2})([a-fd]{2})$/i.exec(hex)
return result
? {
r: parseInt(result[1], 16), // FF → 255
g: parseInt(result[2], 16), // 57 → 87
b: parseInt(result[3], 16), // 33 → 51
}
: null
}
hexToRgb('#FF5733') // → { r: 255, g: 87, b: 51 }
// RGB to HEX
function rgbToHex(r: number, g: number, b: number): string {
return '#' + [r, g, b]
.map(v => v.toString(16).padStart(2, '0'))
.join('')
.toUpperCase()
}
rgbToHex(255, 87, 51) // → '#FF5733'HEX is the right choice for static values in CSS and when passing colors to third-party APIs or SVG attributes that expect #RRGGBB strings. It is a poor choice whenever you need to manipulate a color — darkening #FF5733 requires converting to RGB or HSL first, doing the math, and converting back.
RGB: The Native Color Model of Screens
RGB (Red, Green, Blue) directly maps to how LCD and OLED screens work: each pixel has three sub-pixels that emit red, green, and blue light at varying intensities. The RGB model has been supported in CSS since CSS2 (1998). The modern syntax dropped the mandatory comma and added opacity support inline:
/* Legacy syntax (still valid) */
color: rgb(255, 87, 51);
color: rgba(255, 87, 51, 0.5); /* 50% opacity */
/* Modern CSS Color Level 4 syntax (CSS4, all modern browsers) */
color: rgb(255 87 51);
color: rgb(255 87 51 / 50%); /* Alpha as percentage */
color: rgb(255 87 51 / 0.5); /* Alpha as decimal */
/* Percentage values are valid too */
color: rgb(100% 34% 20%);RGB shines for programmatic channel manipulation — adding red to a color, adjusting brightness by scaling all channels proportionally, or mixing two colors:
// Linear blend between two RGB colors
function mixColors(
color1: { r: number; g: number; b: number },
color2: { r: number; g: number; b: number },
ratio: number // 0.0 = 100% color1, 1.0 = 100% color2
) {
return {
r: Math.round(color1.r + (color2.r - color1.r) * ratio),
g: Math.round(color1.g + (color2.g - color1.g) * ratio),
b: Math.round(color1.b + (color2.b - color1.b) * ratio),
}
}
// Note: for perceptually smooth blending, convert to OKLCH first.
// RGB blending through gray (e.g., blue + yellow = muddy brown),
// whereas OKLCH blending produces vivid intermediate hues.HSL: The Designer's Color Model
HSL (Hue, Saturation, Lightness) was introduced to CSS in CSS3 and is the most intuitive format for humans to reason about. Instead of describing how much red, green, and blue to mix, you specify which color (hue), how vivid it is (saturation), and how bright it is (lightness).
/* hsl(hue, saturation%, lightness%) */
color: hsl(9, 100%, 60%); /* vivid orange-red */
color: hsl(9, 100%, 40%); /* darker orange-red */
color: hsl(9, 100%, 80%); /* lighter orange-red (pastel) */
color: hsl(9, 0%, 60%); /* gray (saturation 0%) */
/* Modern CSS4 syntax (no commas) */
color: hsl(9 100% 60%);
color: hsl(9 100% 60% / 50%); /* with 50% opacity */
/* Hue rotation creates complementary colors */
/* Complement of orange (9°) is blue-green (189°) */
color: hsl(189 100% 60%);Generating a Full Palette from One Hue
HSL makes palette generation trivial. A 10-stop gray scale, a full set of tints and shades, or a triad of harmonious colors all follow from simple arithmetic:
// Generate a 9-stop shade palette from a base hue and saturation
function generatePalette(hue: number, saturation: number): string[] {
const stops = [95, 88, 75, 60, 50, 40, 30, 20, 10]
return stops.map(l => `hsl(${hue} ${saturation}% ${l}%)`)
}
// Usage: generatePalette(220, 90)
// → ["hsl(220 90% 95%)", "hsl(220 90% 88%)", ... "hsl(220 90% 10%)"]
// Triadic harmony: hues 120° apart
function triad(baseHue: number): [string, string, string] {
return [
`hsl(${baseHue} 80% 55%)`,
`hsl(${(baseHue + 120) % 360} 80% 55%)`,
`hsl(${(baseHue + 240) % 360} 80% 55%)`,
]
}
// CSS custom properties for a themeable design system
:root {
--brand-hue: 220;
--brand-sat: 90%;
--color-50: hsl(var(--brand-hue) var(--brand-sat) 95%);
--color-500: hsl(var(--brand-hue) var(--brand-sat) 50%);
--color-900: hsl(var(--brand-hue) var(--brand-sat) 10%);
}RGB ↔ HSL Conversion
function rgbToHsl(r: number, g: number, b: number) {
r /= 255; g /= 255; b /= 255
const max = Math.max(r, g, b), min = Math.min(r, g, b)
const l = (max + min) / 2
let h = 0, s = 0
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, 87, 51) // → { h: 9, s: 100, l: 60 }For more on when each CSS color format is appropriate in different contexts, see the Color Formats Explained article for a deeper dive into the theory.
OKLCH: The Modern Standard (and Why It Fixes HSL)
HSL has a fundamental problem: it is not perceptually uniform. Yellow at hsl(60 80% 50%) looks dramatically brighter than blue at hsl(240 80% 50%), even though both have the same numeric lightness. This is because HSL was defined mathematically rather than perceptually.
OKLCH (Lightness, Chroma, Hue in the OKLab color space, published by Björn Ottosson in 2020) solves this. It is a perceptually uniform polar color space where equal numeric differences produce equal perceived differences. According to Can I Use, OKLCH reached 93%+ global browser support in Q2 2025 (Chrome 111+, Firefox 113+, Safari 15.4+, Edge 111+).
/* OKLCH syntax: oklch(lightness chroma hue) */
/* lightness: 0 = black, 1 = white */
/* chroma: 0 = gray, ~0.4 = max vivid (varies by hue) */
/* hue: 0–360° (same wheel orientation as HSL) */
color: oklch(0.63 0.26 29); /* vivid orange-red */
color: oklch(0.63 0.26 149); /* vivid green (same lightness, same chroma) */
color: oklch(0.63 0.26 269); /* vivid blue (same lightness, same chroma) */
/* All three look equally bright — unlike HSL equivalents */
/* Opacity with slash */
color: oklch(0.63 0.26 29 / 50%);
/* Fallback for older browsers */
.button {
color: #e84314; /* fallback */
color: oklch(0.63 0.26 29); /* modern browsers use this */
}
/* CSS custom properties with OKLCH */
:root {
--brand-l: 0.55;
--brand-c: 0.22;
--brand-h: 265;
--color-primary: oklch(var(--brand-l) var(--brand-c) var(--brand-h));
--color-primary-light: oklch(0.75 var(--brand-c) var(--brand-h));
--color-primary-dark: oklch(0.35 var(--brand-c) var(--brand-h));
}P3 Wide Gamut Colors
One of OKLCH's most important capabilities is access to the P3 color gamut — the expanded range of colors that modern displays (iPhone, MacBook Pro, most 2023+ monitors) can show but that sRGB HEX/RGB/HSL cannot reach. These vivid colors are genuinely unavailable in the older formats.
/* Colors with chroma > ~0.37 may exceed sRGB gamut.
Browsers automatically clamp to the closest sRGB value
on non-P3 displays, so there's no risk of broken output. */
/* sRGB-safe vivid green (max chroma without exceeding gamut) */
color: oklch(0.75 0.25 145);
/* P3 vivid green — only visible on P3 displays */
color: oklch(0.75 0.37 145);
/* Feature query for P3-capable displays */
@media (color-gamut: p3) {
.hero-gradient {
background: linear-gradient(
to right,
oklch(0.65 0.32 265), /* vivid purple */
oklch(0.65 0.30 330) /* vivid pink */
);
}
}Color Format Comparison
| Format | Perceptually Uniform? | P3 Wide Gamut? | Best For | Browser Support |
|---|---|---|---|---|
| #RRGGBB | No | No | Static CSS values, APIs | All browsers |
| rgb() | No | No | Channel manipulation, canvas | All browsers |
| hsl() | Partial | No | Palette generation, theming | All browsers |
| oklch() | Yes | Yes | New design systems, P3 displays | 93%+ (2026) |
| lab() / lch() | Yes | Yes | Legacy perceptual use cases | 93%+ (2026) |
Color Contrast and Accessibility
WCAG 2.1 (Web Content Accessibility Guidelines) defines minimum contrast ratios that any production color pair must meet. The contrast ratio formula uses relative luminance — a measure of how much light a color appears to emit on a calibrated display, defined in WCAG 2.1 by the W3C and based on the IEC 61966-2-1 sRGB specification.
// WCAG 2.1 relative luminance calculation (per W3C spec)
function relativeLuminance(r: number, g: number, b: number): number {
const srgb = [r, g, b].map(v => {
const s = v / 255
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
})
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2]
}
// Contrast ratio between two colors (always >= 1)
function contrastRatio(
fg: { r: number; g: number; b: number },
bg: { r: number; g: number; b: number }
): number {
const L1 = relativeLuminance(fg.r, fg.g, fg.b)
const L2 = relativeLuminance(bg.r, bg.g, bg.b)
const lighter = Math.max(L1, L2)
const darker = Math.min(L1, L2)
return (lighter + 0.05) / (darker + 0.05)
}
// Example: white text on #3B82F6 (Tailwind blue-500)
const white = { r: 255, g: 255, b: 255 }
const blue = { r: 59, g: 130, b: 246 }
console.log(contrastRatio(white, blue).toFixed(2)) // → 3.05
// WCAG AA FAILS for normal text (needs 4.5:1)
// WCAG AA PASSES for large text (needs 3:1)
// This is why white-on-blue-500 is acceptable for headings but not body textWCAG Contrast Level Reference
| Level | Normal Text | Large Text (≥18pt or 14pt bold) | UI Components |
|---|---|---|---|
| WCAG AA | 4.5:1 minimum | 3:1 minimum | 3:1 minimum |
| WCAG AAA | 7:1 minimum | 4.5:1 minimum | Not defined |
According to the WebAIM Million report (2025), 79.7% of home pages have at least one WCAG contrast failure. Low contrast is consistently the most common accessibility failure across the web — and one of the easiest to fix once you have a color picker that shows you the ratio in real time. See our full web accessibility testing guide for the complete WCAG 2.1 checklist.
Building a Themeable Design System with CSS Variables
The most effective pattern for a design system combines CSS custom properties with HSL or OKLCH so that a single variable change re-themes the entire UI. This is how Tailwind CSS v3's arbitrary color utilities work, and it is the approach used by Radix UI, shadcn/ui, and most modern component libraries.
/* Approach 1: HSL component variables */
:root {
/* Define the raw channel values */
--primary-h: 220;
--primary-s: 90%;
/* Derive all shades from those two values */
--primary-50: hsl(var(--primary-h) var(--primary-s) 95%);
--primary-100: hsl(var(--primary-h) var(--primary-s) 90%);
--primary-200: hsl(var(--primary-h) var(--primary-s) 80%);
--primary-500: hsl(var(--primary-h) var(--primary-s) 50%);
--primary-700: hsl(var(--primary-h) var(--primary-s) 35%);
--primary-900: hsl(var(--primary-h) var(--primary-s) 15%);
}
/* Rebrand from blue to purple: change two values */
:root {
--primary-h: 270;
--primary-s: 85%;
}
/* Approach 2: OKLCH (more accurate perceptually) */
:root {
--brand-l: 0.55;
--brand-c: 0.22;
--brand-h: 265;
}
.btn-primary {
background: oklch(var(--brand-l) var(--brand-c) var(--brand-h));
color: oklch(0.98 0 0); /* near-white */
}
.btn-primary:hover {
background: oklch(calc(var(--brand-l) - 0.07) var(--brand-c) var(--brand-h));
}
/* Dark mode: adjust lightness only */
@media (prefers-color-scheme: dark) {
:root { --brand-l: 0.70; }
}The calc() trick for hover states in OKLCH (calc(var(--brand-l) - 0.07)) is significantly cleaner than the HSL equivalent because OKLCH lightness is perceptually linear — subtracting 0.07 always produces a noticeably darker shade regardless of which hue you are on.
Choosing a Color Picker Tool: What to Look For
Not all color pickers are equal. Here is what a professional-grade tool should include:
- Live format conversion — changes in one format (HEX, RGB, HSL, OKLCH) should instantly update all others.
- WCAG contrast checker — input foreground and background colors, see the contrast ratio and AA/AAA pass/fail status immediately.
- Alpha channel support — any real-world UI uses semi-transparent colors for shadows, overlays, and glass effects.
- Color space awareness — the tool should ideally support OKLCH and P3 gamut, not just the sRGB-only formats.
- Copy output in multiple formats — CSS
hsl(), Tailwind config object, design token JSON — not just a HEX string. - Client-side only — your palette is brand-sensitive. A tool that does not send colors to a server avoids unnecessary data exposure.
The BytePane Color Converter handles all format conversions — HEX, RGB, HSL, and OKLCH — entirely in your browser. No data leaves your machine.
Color Conversion Quick Reference
/* The same color in every format */
HEX: #FF5733
RGB: rgb(255 87 51)
HSL: hsl(9 100% 60%)
OKLCH: oklch(0.63 0.26 29)
/* Named colors as reference anchors */
red: #FF0000 rgb(255 0 0) hsl(0 100% 50%)
green: #008000 rgb(0 128 0) hsl(120 100% 25%)
blue: #0000FF rgb(0 0 255) hsl(240 100% 50%)
white: #FFFFFF rgb(255 255 255) hsl(0 0% 100%)
black: #000000 rgb(0 0 0) hsl(0 0% 0%)
gray50: #808080 rgb(128 128 128) hsl(0 0% 50%)Frequently Asked Questions
What is the difference between HEX, RGB, and HSL color formats?
HEX and RGB both describe colors using red, green, and blue channels — they are mathematically equivalent (ff = 255). HSL describes color as Hue (0–360°), Saturation (0–100%), and Lightness (0–100%). HEX is compact for static values. RGB is better for channel manipulation. HSL is the most intuitive for generating palettes, because adjusting lightness or saturation produces predictable results without needing to know the channel math.
How do I convert HEX to RGB?
Split the HEX string into three two-character pairs and convert each from base-16 to decimal: #FF5733 → FF=255, 57=87, 33=51 → rgb(255, 87, 51). In JavaScript: parseInt("FF", 16) returns 255. For 8-digit HEX with alpha, the last pair converts the same way and is divided by 255 to get a 0–1 alpha value.
What is OKLCH and should I use it instead of HSL?
OKLCH is a perceptually uniform color space where equal numeric changes produce equal perceived visual differences. HSL has a known flaw: yellow at 50% lightness looks far brighter than blue at 50% lightness because HSL is not perceptually calibrated. OKLCH corrects this. As of 2026 it has 93%+ global browser support. Use OKLCH for new design systems; provide an HSL or HEX fallback for legacy browser contexts.
How do I check if my colors meet WCAG contrast requirements?
WCAG 2.1 requires a contrast ratio of at least 4.5:1 for normal text (AA level) and 3:1 for large text (18pt or 14pt bold). Calculate contrast using the relative luminance formula defined in the W3C spec, or use a tool that computes it automatically. The BytePane Color Converter shows WCAG pass/fail status for any two colors. Per the WebAIM Million 2025 report, 79.7% of home pages still fail at least one contrast check.
Why does HSL hue use degrees (0–360)?
Hue represents a position on the color wheel, which is circular — hence 360°. Key landmarks: 0°=red, 60°=yellow, 120°=green, 180°=cyan, 240°=blue, 300°=magenta, 360°=red again. Hue values wrap around, so hsl(370deg 80% 50%) is identical to hsl(10deg 80% 50%). This wrapping property is useful for generating analogous or complementary colors by adding/subtracting fixed degree offsets.
What is the alpha channel and how is it expressed in different formats?
Alpha controls opacity. In 8-digit HEX it is a two-digit suffix (#RRGGBBAA, where FF=fully opaque, 00=fully transparent). In RGB: rgba(255, 87, 51, 0.5). In HSL: hsla(9, 100%, 60%, 0.5). Modern CSS4 uses slash notation: rgb(255 87 51 / 50%) or hsl(9 100% 60% / 50%).
Can I use CSS variables with color functions to build a theme?
Yes — and this is the recommended pattern for design systems. Define base hue and saturation (or OKLCH lightness and chroma) as CSS custom properties, then derive all shades using hsl() or oklch(). Changing --brand-hue from 220 to 160 instantly re-themes the entire application without touching any other variable. This is how shadcn/ui and other modern component libraries implement multiple color themes with minimal CSS duplication.
Convert Colors Instantly in Your Browser
The BytePane Color Converter converts between HEX, RGB, HSL, and OKLCH in real time. It also calculates WCAG contrast ratios for any color pair — fully client-side, no tracking, no API calls.
Open Color ConverterRelated Articles
Color Formats Explained: HEX, RGB, HSL & Beyond
Deep dive into color format theory, conversion formulas, and design system patterns.
Web Accessibility Testing Guide
WCAG 2.1 compliance checklist, contrast requirements, and testing tools.
CSS Grid vs Flexbox
When to use each layout system with practical examples.
CSS :has() Selector Guide
The parent selector CSS always needed, with real-world patterns.