DevToys Web Pro iconDevToys Web Proব্লগ
অনূদিত LocalePack logoLocalePack
আমাদের রেট দিন:
ব্রাউজার এক্সটেনশন ব্যবহার করে দেখুন:
← Back to Blog

Color Picker Guide: RGB, HSL, OKLCH, and Color Conversion Explained

12 min read

Color models are not interchangeable. RGB maps to how displays emit light. HSL maps to how designers think about hue relationships. OKLCH maps to how human vision actually perceives brightness. Picking the wrong model for the job produces either rounding errors in conversion, inconsistent lightness steps in a palette, or colors that look different on wide-gamut screens than intended. Use the Color Picker to follow along with the conversions in this guide. You can browse all color tools under the Colors category.

This guide covers each major color model, when to use it in CSS, the conversion math between them, a CSS custom properties workflow, and common pitfalls. Related reading: Color Contrast and WCAG and Designing for Color Blindness.

RGB and Hex — Machine-Native Color

RGB (Red, Green, Blue) is the native color model for digital displays. Each channel is an integer from 0 to 255, giving 16,777,216 possible colors. The hex format #rrggbb is just those three bytes written in base-16.

/* All four representations of the same color */
color: rgb(99, 102, 241);      /* decimal */
color: rgb(38.8% 40% 94.5%);  /* percentage */
color: #6366f1;                /* hex shorthand */
color: #6366f1ff;              /* hex with full-opacity alpha */

The four-byte form #rrggbbaa adds an alpha channel as the fourth byte. #6366f180 is 50% opacity — the 80 is 128/255 ≈ 0.502 expressed in hex. Browser support for eight-digit hex is universal as of Chrome 62, Firefox 49, and Safari 10.

NotationAlphaUse when
#rrggbbNoToken values, design handoff, copy-paste from Figma
#rrggbbaaYesOverlays, shadows, translucent backgrounds
rgb(r g b)No (add / alpha)Dynamic values via CSS custom properties
rgb(r g b / a)YesTransparency derived from a base color channel

When to prefer RGB/hex: design token values, color output from a design tool, HTTP API responses, canvas drawing, WebGL, or any context where you need a stable byte-level representation without perceptual transformations. Hex is compact, unambiguous, and universally supported.

When to avoid hex: generating palette variations (tints, shades, analogous colors). Nudging a hex value by a fixed amount in any channel does not produce visually uniform steps. Use HSL or OKLCH for that.

HSL — Designer-Friendly Color

HSL (Hue, Saturation, Lightness) reframes RGB into three properties that match how designers reason about color:

  • Hue — angle on the color wheel, 0–360deg. 0/360 = red, 120 = green, 240 = blue.
  • Saturation — 0% is gray, 100% is fully saturated.
  • Lightness — 0% is black, 50% is the "pure" hue, 100% is white.
/* HSL in CSS */
color: hsl(239deg 84% 67%);          /* Indigo — same as #6366f1 */
color: hsl(239deg 84% 67% / 0.5);   /* 50% transparent */

/* Palette steps using HSL — easy to read */
--brand-100: hsl(239deg 84% 95%);
--brand-500: hsl(239deg 84% 67%);
--brand-900: hsl(239deg 84% 25%);

HSL makes palette construction intuitive: hold hue and saturation constant, vary only lightness. This is why Tailwind CSS, Material Design, and most design systems define their color scales in HSL-derived values.

The catch: HSL lightness is not perceptually uniform. hsl(60deg 100% 50%) (yellow) looks much brighter to the human eye than hsl(240deg 100% 50%) (blue) even though both have L=50%. This means two colors with the same HSL lightness can have very different perceived contrast — a problem that OKLCH solves.

HSV vs HSL — Value vs Lightness

HSV (Hue, Saturation, Value) — sometimes called HSB (Hue, Saturation, Brightness) — uses "Value" instead of "Lightness". The difference is significant:

PropertyHSLHSV/HSB
Pure hue (red, green, blue)L=50%V=100%
WhiteL=100%S=0%, V=100%
BlackL=0%V=0%
Tints (add white)Increase L toward 100%Decrease S toward 0%
Shades (add black)Decrease L toward 0%Decrease V toward 0%

Figma uses HSL for its color picker controls. Photoshop and Illustrator use HSB. When converting values between the two tools, you must convert the model — they are not the same number space. The Color Picker shows all representations simultaneously so you can see the difference.

HSV is rarely used in CSS directly. It appears in color picker UI (intuitive drag behavior) and in some image processing algorithms. For CSS output, always convert to HSL, RGB/hex, or OKLCH.

OKLCH — The Modern Choice

OKLCH (OK Lightness Chroma Hue) is a perceptually uniform color space added to CSS in 2023. "Perceptually uniform" means that equal numeric steps produce visually equal steps — something neither RGB nor HSL can guarantee.

  • L — lightness, 0 (black) to 1 (white), perceptually linear
  • C — chroma (colorfulness), 0 (gray) to ~0.37 for highly saturated sRGB colors
  • H — hue angle, 0–360deg, same as HSL but perceptually corrected
/* OKLCH in CSS */
color: oklch(0.62 0.19 264deg);   /* Indigo — same as #6366f1 */
color: oklch(0.62 0.19 264deg / 0.5);  /* with alpha */

/* Palette with uniform perceived lightness steps */
--brand-100: oklch(0.96 0.04 264deg);
--brand-300: oklch(0.80 0.10 264deg);
--brand-500: oklch(0.62 0.19 264deg);
--brand-700: oklch(0.44 0.17 264deg);
--brand-900: oklch(0.28 0.10 264deg);

The key advantage: when you generate a 10-step palette by varying L from 0.1 to 0.95 in equal increments, the visual brightness steps are actually uniform — unlike HSL where the same approach produces uneven results (yellow jumps, blue barely moves).

Browser support as of 2026: Chrome 111+ (March 2023), Firefox 113+ (May 2023), Safari 15.4+ (March 2022). All modern evergreen browsers support OKLCH natively. No polyfill is needed for production use targeting these browsers.

Fallback pattern for older targets:

/* Fallback for browsers without OKLCH support */
.button {
  background-color: #6366f1;                    /* sRGB fallback */
  background-color: oklch(0.62 0.19 264deg);   /* modern override */
}

CSS processes declarations in order and ignores values it does not understand. The oklch() line silently replaces the hex fallback in browsers that support it, and is silently ignored in those that do not.

sRGB vs P3 vs Rec.2020 — Color Gamuts

A color gamut is the range of colors a device can display. Most of the CSS history has assumed sRGB — the gamut of older monitors. Modern displays (Apple Retina since 2016, iPhone since iPhone 7, most flagship Android phones) support Display P3, which covers roughly 25% more colors than sRGB. Rec.2020 is wider still but is not common in consumer hardware yet.

Color spaceCoverageCSS syntaxHardware
sRGB~35% of visible colors#rrggbb, rgb(), hsl()Universal
Display P3~45% of visible colorscolor(display-p3 r g b)Apple (2016+), flagship Android
Rec.2020~75% of visible colorscolor(rec2020 r g b)Professional monitors, HDR TV
/* Wide-gamut color with sRGB fallback */
.hero-text {
  color: #e8434a;                            /* sRGB fallback */
  color: color(display-p3 0.91 0.26 0.29);  /* vivid P3 red */
}

/* Using @media to target wide-gamut displays */
@media (color-gamut: p3) {
  .accent {
    background-color: color(display-p3 0.00 0.80 0.40);
  }
}

The color(display-p3 ...) channels are normalized 0–1 (not 0–255). A P3 color that is "out of sRGB gamut" will be clamped to the nearest sRGB color by browsers that only support sRGB, so the fallback must be defined explicitly. OKLCH can address P3 colors naturally — any OKLCH value with chroma C above approximately 0.22 may fall outside sRGB and into P3 territory.

Conversion Math — Precision and Rounding

Converting between color models introduces rounding error. The issue is the 0–255 integer range of RGB: dividing by 255 and then multiplying back rarely returns exactly the same integer. Round-tripping HSL to RGB and back may lose one or two values in a channel.

HSL to RGB (JavaScript)

/**
 * Convert HSL to RGB.
 * @param {number} h - hue in degrees [0, 360)
 * @param {number} s - saturation [0, 1]
 * @param {number} l - lightness [0, 1]
 * @returns {{ r: number, g: number, b: number }} each channel 0–255
 */
function hslToRgb(h, s, l) {
  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),
  };
}

// Example
hslToRgb(239, 0.84, 0.67);
// => { r: 99, g: 102, b: 241 }  (matches #6366f1)

RGB to HSL (JavaScript)

/**
 * Convert RGB to HSL.
 * @param {number} r - red 0–255
 * @param {number} g - green 0–255
 * @param {number} b - blue 0–255
 * @returns {{ h: number, s: number, l: number }} h in degrees, s and l in [0,1]
 */
function rgbToHsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const d = max - min;
  let h = 0;
  const l = (max + min) / 2;
  const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));

  if (d !== 0) {
    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, l };
}

// Example
rgbToHsl(99, 102, 241);
// => { h: 239, s: 0.839..., l: 0.667... }

Hex to RGB and Back

function hexToRgb(hex) {
  const n = parseInt(hex.replace('#', ''), 16);
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}

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

hexToRgb('#6366f1');     // { r: 99, g: 102, b: 241 }
rgbToHex(99, 102, 241); // '#6366f1'

Rounding discipline: always carry floating-point values through intermediate steps and round only at the final output. Rounding at each step compounds error. The Color Picker keeps full precision internally and rounds only when displaying values.

HSL round-trip loss: converting #6366f1 to HSL gives hsl(239 84% 67%) when rounded to whole numbers. Converting that back gives rgb(99, 102, 240) — one unit off on the blue channel. Store your canonical color values in hex or as OKLCH floats, not in rounded HSL integers.

CSS Custom Properties Workflow

CSS custom properties (variables) are the standard way to manage a color system in a codebase. Define your palette at the :root level and reference the tokens everywhere.

/* tokens.css — palette definition */
:root {
  /* Brand palette in OKLCH — perceptually uniform steps */
  --brand-50:  oklch(0.97 0.02 264deg);
  --brand-100: oklch(0.93 0.05 264deg);
  --brand-200: oklch(0.86 0.09 264deg);
  --brand-300: oklch(0.77 0.13 264deg);
  --brand-400: oklch(0.70 0.16 264deg);
  --brand-500: oklch(0.62 0.19 264deg);  /* primary */
  --brand-600: oklch(0.54 0.18 264deg);
  --brand-700: oklch(0.44 0.17 264deg);
  --brand-800: oklch(0.35 0.13 264deg);
  --brand-900: oklch(0.26 0.09 264deg);
  --brand-950: oklch(0.18 0.06 264deg);

  /* Semantic aliases */
  --color-primary:         var(--brand-500);
  --color-primary-hover:   var(--brand-600);
  --color-primary-subtle:  var(--brand-50);

  /* Neutral scale */
  --gray-100: oklch(0.97 0.00 0deg);
  --gray-900: oklch(0.15 0.00 0deg);
  --color-text:       var(--gray-900);
  --color-background: var(--gray-100);
}

/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
  :root {
    --color-text:       var(--gray-100);
    --color-background: var(--gray-900);
  }
}

The two-layer pattern — palette tokens (--brand-500) and semantic tokens (--color-primary) — decouples what the color is from what it means. Swapping the brand color only requires changing the palette token; all semantic references update automatically.

/* Component usage — reference semantic tokens, never raw values */
.button-primary {
  background-color: var(--color-primary);
  color: #ffffff;
}
.button-primary:hover {
  background-color: var(--color-primary-hover);
}

/* Combining custom properties with oklch for derived alpha */
.card-overlay {
  /* Decompose oklch into channels for dynamic alpha */
  --overlay-l: 0.62;
  --overlay-c: 0.19;
  --overlay-h: 264deg;
  background-color: oklch(var(--overlay-l) var(--overlay-c) var(--overlay-h) / 0.15);
}

Design Tokens and Cross-Platform Export

Design tokens extend CSS custom properties to a platform-agnostic format that can be compiled to Swift, Kotlin, JSON for web, or XML for Android. The two dominant toolchains are Style Dictionary (Amazon) and Tokens Studio (Figma plugin).

Style Dictionary

// tokens/color.json
{
  "color": {
    "brand": {
      "500": { "value": "#6366f1", "type": "color" },
      "600": { "value": "#4f46e5", "type": "color" }
    },
    "primary": {
      "default": { "value": "{color.brand.500}", "type": "color" }
    }
  }
}
// style-dictionary.config.js
module.exports = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      prefix: 'color',
      buildPath: 'dist/',
      files: [{ destination: 'tokens.css', format: 'css/variables' }],
    },
    ios: {
      transformGroup: 'ios-swift',
      buildPath: 'ios/Sources/',
      files: [{ destination: 'ColorTokens.swift', format: 'ios-swift/class.swift' }],
    },
    android: {
      transformGroup: 'android',
      buildPath: 'android/src/main/res/values/',
      files: [{ destination: 'colors.xml', format: 'android/resources' }],
    },
  },
};

Running npx style-dictionary build compiles the JSON into CSS variables, Swift color literals, and Android XML resources from the same source of truth.

Tokens Studio (Figma)

Tokens Studio stores token values directly in a Figma file and can sync to a GitHub repository. A typical CI workflow:

  1. Designer changes token in Figma via Tokens Studio plugin.
  2. Plugin pushes a JSON file to a branch.
  3. CI runs Style Dictionary to compile updated tokens.
  4. Pull request with regenerated CSS/Swift/Kotlin is opened for review.

This eliminates the manual "design to code" translation step and ensures every platform is always in sync with the design source.

Common Pitfalls

Browser Color Clamping

If you specify an OKLCH or P3 color that is outside the sRGB gamut on a display that only supports sRGB, the browser clamps the value to the nearest in-gamut color. This is silent — there is no warning. Use the DevTools color picker or the Color Picker to check whether your color is in-gamut for sRGB before assuming it will look the same everywhere.

/* This vivid green is outside sRGB — will be clamped on sRGB displays */
color: oklch(0.75 0.30 145deg);

/* Explicitly provide an sRGB fallback */
color: #22c55e;
color: oklch(0.75 0.30 145deg);

currentColor Inheritance

currentColor resolves to the element's computed color value. This creates implicit inheritance chains that break when a parent's color changes.

/* Trap: .icon inherits --color-primary through currentColor */
.button { color: var(--color-primary); }
.button svg { fill: currentColor; }  /* fine */

/* Problem: adding a text-color override breaks the icon */
.button.danger { color: var(--color-danger); }
/* Now the icon is also danger-colored — probably not intended */

/* Fix: use an explicit token for the icon */
.button svg { fill: var(--button-icon-color, currentColor); }

Named Color Gotchas

CSS named colors seem convenient but have surprising values. gray is #808080, but darkgray is #a9a9a9 — lighter than gray. green is #008000, not #00ff00 (which is lime). Named colors have no predictable relationship to each other. Avoid them in design systems; use explicit token values.

Named colorHex valueSurprise
gray#808080Middle gray
darkgray#a9a9a9Lighter than gray
green#008000Not full-brightness green
lime#00ff00Full-brightness green
cyan#00ffffSame as aqua

Percentage vs Number Syntax in Modern CSS

CSS Color Level 4 added the "space-separated with slash alpha" syntax for all color functions. Both of these are valid, but mixing old and new syntax in the same codebase causes confusion:

/* Legacy comma syntax */
color: rgb(99, 102, 241);
color: hsl(239deg, 84%, 67%);

/* Modern space syntax (CSS Color Level 4) */
color: rgb(99 102 241);
color: hsl(239deg 84% 67%);
color: hsl(239deg 84% 67% / 0.5);  /* alpha with slash */

/* oklch and color() only use modern syntax */
color: oklch(0.62 0.19 264deg);
color: color(display-p3 0.39 0.40 0.95);

The comma syntax is deprecated in Color Level 4 but still supported everywhere. New code should use the space syntax for consistency with oklch() and color(), which only support the space-separated form.


Convert and inspect all these color formats directly in your browser with the Color Picker — it shows RGB, hex, HSL, HSV, and OKLCH simultaneously with no data leaving your machine. Browse all color utilities under Colors.