BytePane

HEX to RGB Converter: Convert Color Codes Instantly

CSS & Colors12 min read

The Copy-Paste That Breaks Your Animation

You copy a brand color from Figma: #4F46E5. CSS? Works fine. Now you need to animate it — fade from 30% opacity to 100% on hover. You reach for rgba()... and realize HEX doesn't take an alpha parameter directly. Or you're writing a canvas animation and the drawing API only accepts separate R, G, B integers. Or you need to darken a color by 20% in JavaScript without pulling in a library.

Every frontend developer hits this at least weekly. The fix is trivial once you understand it — and understanding the math means you can implement it in any language, any context, without a dependency. This article covers the full HEX-to-RGB conversion: the formula, every edge case (3-digit shorthand, 8-digit alpha), and production-ready code for JavaScript, TypeScript, Python, and Go.

Need a quick conversion right now? Use BytePane's color converter tool — supports HEX, RGB, HSL, and OKLCH with live preview.

Key Takeaways

  • HEX and RGB are the same color model — just different notations. Both represent red, green, and blue channels. No visual difference whatsoever.
  • Conversion: split 6-digit HEX into three 2-char pairs, parse each as base-16. parseInt('FF', 16) → 255.
  • 3-digit shorthand: #F53 expands to #FF5533 by doubling each digit before parsing.
  • 8-digit codes include alpha: #FF573380 → alpha = 128/255 ≈ 0.502. Not a percentage — a fraction of 255.
  • According to the HTTP Archive 2025 Web Almanac, HEX is present in over 90% of CSS stylesheets — it's the dominant web color format by usage volume.

The Math: Why HEX and RGB Are the Same Thing

A CSS color is three values: red intensity, green intensity, and blue intensity. Each channel ranges from 0 (none) to 255 (maximum). That's 256 possible values per channel — exactly one byte, exactly two hexadecimal digits.

A 6-digit HEX color is just those three byte values concatenated in hex encoding: #RRGGBB. The # is a prefix, not data. To recover the integers, parse each pair:

#4F46E5

#  4F   46   E5
   │    │    └── Blue:  0xE5 = 14×16 + 5 = 224 + 5  = 229
   │    └─────── Green: 0x46 =  4×16 + 6 =  64 + 6  =  70
   └──────────── Red:   0x4F =  4×16 + 15 = 64 + 15 =  79

→ rgb(79, 70, 229)  ← Indigo 600 in Tailwind CSS

That's the complete algorithm. For a 6-digit HEX, there are exactly three steps: strip the #, split into three 2-character substrings, convert each from hex to decimal. Everything else is just handling variants.

HEX Character Map

Hexadecimal uses 16 digits: 0–9 map directly to their decimal values, and A–F (or a–f) represent 10–15. HEX is case-insensitive — #ff5733 and #FF5733 are identical.

Hex0–9ABCDEF
Decimal0–9101112131415
Range per pair00 (=0) to FF (=255) — one byte, 256 possible values

Every HEX Format You'll Encounter

3-Digit Shorthand (#RGB)

The 3-digit shorthand is valid only when each channel value is a repeated digit pair — like #AABBCC#ABC. To expand: double each digit, then convert. #F53#FF5533 → rgb(255, 85, 51).

A common mistake is treating #F53 as if F=0xF0=240, 5=0x50=80, 3=0x30=48 — i.e., padding on the right instead of repeating. That's wrong. The CSS spec defines it as duplication, not zero-padding: F → FF, not F0.

// 3-digit HEX examples
#F53 → #FF5533 → rgb(255, 85, 51)   ← coral
#0AF → #00AAFF → rgb(0, 170, 255)   ← sky blue
#FFF → #FFFFFF → rgb(255, 255, 255) ← white
#000 → #000000 → rgb(0, 0, 0)       ← black
#ABC → #AABBCC → rgb(170, 187, 204) ← blue-gray

8-Digit HEX with Alpha (#RRGGBBAA)

CSS Color Level 4 (supported in all modern browsers) adds an optional alpha byte as the last two hex digits. The alpha value is not a percentage — it's another 0–255 range. To convert to the 0.0–1.0 CSS alpha scale, divide by 255.

Alpha HexDecimal÷ 255Opacity
FF2551.000100% (fully opaque)
CC2040.80080%
801280.502~50%
40640.25125%
1A260.102~10%
0000.0000% (fully transparent)
#4F46E5CC
  │    │    │  └─ Alpha: CC = 204 → 204/255 = 0.80
  │    │    └──── Blue:  E5 = 229
  │    └───────── Green: 46 = 70
  └────────────── Red:   4F = 79

→ rgba(79, 70, 229, 0.8)  ← 80% opaque indigo

4-Digit Shorthand (#RGBA)

Like 3-digit RGB shorthand but includes alpha. #F53C expands to #FF5533CC → rgba(255, 85, 51, 0.8). Less commonly seen than the 3-digit form but fully valid per the CSS spec.

Format Variants at a Glance

FormatExampleExpands ToAlpha?
#RRGGBB#4F46E5rgb(79, 70, 229)No
#RGB#F53#FF5533 → rgb(255, 85, 51)No
#RRGGBBAA#4F46E5CCrgba(79, 70, 229, 0.8)Yes (last 2 digits)
#RGBA#F53C#FF5533CC → rgba(255, 85, 51, 0.8)Yes (last digit)

Production Code: HEX to RGB in Every Language

JavaScript / TypeScript

The standard approach uses a regex to capture the three (or four) hex pairs, with a normalization step for 3-digit shorthand. The function below handles all four HEX variants and returns null for invalid input rather than throwing.

// TypeScript — handles all HEX variants
interface RgbColor {
  r: number
  g: number
  b: number
  a?: number  // 0.0 – 1.0, only present for 8-digit / 4-digit HEX
}

function hexToRgb(hex: string): RgbColor | null {
  // Normalize: strip leading # and expand 3/4-digit shorthand
  const normalized = hex
    .replace(/^#/, '')
    .replace(/^([a-fd])([a-fd])([a-fd])([a-fd])?$/i, '$1$1$2$2$3$3$4$4')

  const match = normalized.match(/^([a-fd]{2})([a-fd]{2})([a-fd]{2})([a-fd]{2})?$/i)
  if (!match) return null

  const result: RgbColor = {
    r: parseInt(match[1], 16),
    g: parseInt(match[2], 16),
    b: parseInt(match[3], 16),
  }

  if (match[4]) {
    result.a = Math.round((parseInt(match[4], 16) / 255) * 1000) / 1000
  }

  return result
}

// Examples
hexToRgb('#4F46E5')     // { r: 79, g: 70, b: 229 }
hexToRgb('#F53')        // { r: 255, g: 85, b: 51 }
hexToRgb('#4F46E5CC')   // { r: 79, g: 70, b: 229, a: 0.8 }
hexToRgb('#F53C')       // { r: 255, g: 85, b: 51, a: 0.8 }
hexToRgb('invalid')     // null

// Format as CSS string
function hexToCss(hex: string): string | null {
  const c = hexToRgb(hex)
  if (!c) return null
  return c.a !== undefined
    ? `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`
    : `rgb(${c.r}, ${c.g}, ${c.b})`
}

hexToCss('#4F46E5')    // "rgb(79, 70, 229)"
hexToCss('#4F46E5CC')  // "rgba(79, 70, 229, 0.8)"

// Inverse: RGB to HEX
function rgbToHex(r: number, g: number, b: number, a?: number): string {
  const hex = [r, g, b, ...(a !== undefined ? [Math.round(a * 255)] : [])]
    .map(v => v.toString(16).padStart(2, '0'))
    .join('')
  return '#' + hex.toUpperCase()
}

rgbToHex(79, 70, 229)         // '#4F46E5'
rgbToHex(79, 70, 229, 0.8)    // '#4F46E5CC'

Python

from dataclasses import dataclass
from typing import Optional
import re

@dataclass
class RgbColor:
    r: int
    g: int
    b: int
    a: Optional[float] = None  # 0.0 – 1.0

def hex_to_rgb(hex_str: str) -> Optional[RgbColor]:
    # Normalize: strip #, expand 3/4-digit shorthand
    h = hex_str.lstrip('#')
    if len(h) in (3, 4):
        h = ''.join(c * 2 for c in h)

    match = re.fullmatch(r'([0-9a-fA-F]{2})' * (4 if len(h) == 8 else 3), h)
    if not match:
        return None

    r, g, b = (int(match.group(i), 16) for i in range(1, 4))
    a = round(int(match.group(4), 16) / 255, 3) if len(h) == 8 else None
    return RgbColor(r=r, g=g, b=b, a=a)

def hex_to_css(hex_str: str) -> Optional[str]:
    c = hex_to_rgb(hex_str)
    if c is None:
        return None
    if c.a is not None:
        return f"rgba({c.r}, {c.g}, {c.b}, {c.a})"
    return f"rgb({c.r}, {c.g}, {c.b})"

# Examples
hex_to_rgb('#4F46E5')    # RgbColor(r=79, g=70, b=229, a=None)
hex_to_rgb('#F53')       # RgbColor(r=255, g=85, b=51, a=None)
hex_to_rgb('#4F46E5CC')  # RgbColor(r=79, g=70, b=229, a=0.8)

hex_to_css('#4F46E5CC')  # "rgba(79, 70, 229, 0.8)"

# Inverse: RGB to HEX
def rgb_to_hex(r: int, g: int, b: int, a: Optional[float] = None) -> str:
    parts = [r, g, b]
    if a is not None:
        parts.append(round(a * 255))
    return '#' + ''.join(f'{v:02X}' for v in parts)

Go

package color

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

type RGBColor struct {
    R, G, B int
    A       float64  // -1 means not set; 0.0–1.0 otherwise
}

func HexToRGB(hex string) (RGBColor, error) {
    hex = strings.TrimPrefix(hex, "#")

    // Expand 3/4-digit shorthand
    if len(hex) == 3 || len(hex) == 4 {
        expanded := make([]byte, len(hex)*2)
        for i, c := range []byte(hex) {
            expanded[i*2] = c
            expanded[i*2+1] = c
        }
        hex = string(expanded)
    }

    if len(hex) != 6 && len(hex) != 8 {
        return RGBColor{}, fmt.Errorf("invalid hex color: #%s", hex)
    }

    parse := func(s string) (int, error) {
        n, err := strconv.ParseInt(s, 16, 32)
        return int(n), err
    }

    r, err := parse(hex[0:2])
    if err != nil { return RGBColor{}, err }
    g, err := parse(hex[2:4])
    if err != nil { return RGBColor{}, err }
    b, err := parse(hex[4:6])
    if err != nil { return RGBColor{}, err }

    result := RGBColor{R: r, G: g, B: b, A: -1}

    if len(hex) == 8 {
        a, err := parse(hex[6:8])
        if err != nil { return RGBColor{}, err }
        result.A = float64(a) / 255.0
    }

    return result, nil
}

func (c RGBColor) CSS() string {
    if c.A >= 0 {
        return fmt.Sprintf("rgba(%d, %d, %d, %.3f)", c.R, c.G, c.B, c.A)
    }
    return fmt.Sprintf("rgb(%d, %d, %d)", c.R, c.G, c.B)
}

Real-World Use Cases Where You Need RGB Values

Canvas and WebGL Drawing APIs

The HTML Canvas API's fillStyle and strokeStyle accept CSS strings, so you can pass HEX directly. But when you're building a color interpolation function for animations or drawing pixel buffers into ImageData, you need raw integers.

// Interpolate between two HEX colors for animation
function interpolateHex(hex1: string, hex2: string, t: number): string {
  const c1 = hexToRgb(hex1)!
  const c2 = hexToRgb(hex2)!

  const r = Math.round(c1.r + (c2.r - c1.r) * t)
  const g = Math.round(c1.g + (c2.g - c1.g) * t)
  const b = Math.round(c1.b + (c2.b - c1.b) * t)

  return `rgb(${r}, ${g}, ${b})`
}

// Fade from indigo to coral over 60 frames
for (let frame = 0; frame < 60; frame++) {
  const color = interpolateHex('#4F46E5', '#FF5733', frame / 59)
  ctx.fillStyle = color
  ctx.fillRect(0, 0, canvas.width, canvas.height)
}

// Writing to ImageData (pixel buffer) — needs raw integers
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const { r, g, b } = hexToRgb('#4F46E5')!
for (let i = 0; i < imageData.data.length; i += 4) {
  imageData.data[i]     = r  // Red
  imageData.data[i + 1] = g  // Green
  imageData.data[i + 2] = b  // Blue
  imageData.data[i + 3] = 255  // Alpha (0–255, not 0.0–1.0)
}

Dynamic Transparency in CSS Custom Properties

CSS doesn't let you add transparency to a HEX color directly using opacity on the color (that property affects the element, not just the background). The modern pattern is to store color channels as CSS custom properties, then compose with rgba():

/* Old approach: can't easily add transparency to HEX */
:root { --brand: #4F46E5; }
.overlay { background: var(--brand); opacity: 0.8; }  /* Affects WHOLE element */

/* Better: store as R, G, B channels */
:root {
  --brand-r: 79;
  --brand-g: 70;
  --brand-b: 229;
}
.overlay   { background: rgb(var(--brand-r) var(--brand-g) var(--brand-b)); }
.overlay-80 { background: rgb(var(--brand-r) var(--brand-g) var(--brand-b) / 0.8); }
.overlay-40 { background: rgb(var(--brand-r) var(--brand-g) var(--brand-b) / 0.4); }

/* Modern CSS Color Level 4 — even cleaner */
:root { --brand: #4F46E5; }
.overlay-50 { background: color-mix(in srgb, var(--brand) 50%, transparent); }

Programmatic Color Darkening / Lightening

Common in component libraries: hover states that darken a brand color by 15%. Without a color library, you need RGB values to clamp-adjust each channel:

function darkenHex(hex: string, amount: number): string {
  const { r, g, b } = hexToRgb(hex)!
  const clamp = (v: number) => Math.max(0, Math.min(255, v))
  return rgbToHex(
    clamp(r - amount),
    clamp(g - amount),
    clamp(b - amount),
  )
}

darkenHex('#4F46E5', 30)   // Darken by 30: '#301CE3' ← slightly darker indigo

// Note: for perceptually uniform darkening, convert to HSL and reduce L,
// or use OKLCH. Simple RGB subtraction works but is not perceptually linear.

Do You Need a Color Library?

For simple HEX↔RGB conversion, the native code above is sufficient — no dependency needed. Color libraries become worthwhile when you need perceptually uniform transformations (lightening, darkening, mixing that looks right across hues), conversion to HSL/OKLCH/CMYK, or accessibility contrast calculations per WCAG.

LibraryWeekly Downloads (npm)Gzipped SizeBest For
chroma.js~5M~13 KBFull-featured: many color spaces, interpolation
tinycolor2~11M~5 KBBroad format support, legacy compatibility
color~8M~3 KBSimple manipulation, good TypeScript types
culori~1.5M~3 KB tree-shakeableOKLCH/P3 support, modern color spaces
Native codeN/A0 KBHEX↔RGB only, no dependencies needed

Per npm download data (April 2026), tinycolor2 leads the simple color manipulation space at ~11M weekly installs, mostly due to its age and broad ecosystem support. For new projects, culori is worth considering if you're working with OKLCH or wide-gamut P3 displays. For HEX↔RGB only, write the 15-line function — it's not worth a dependency.

For a deeper dive into all CSS color formats — including HSL, OKLCH, and display-p3 — see the CSS color formats guide.

Bugs Developers Actually Ship

Forgetting 3-Digit Expansion

parseInt('F3', 16) works fine for 6-digit HEX. But slicing #F53 at positions 1, 3, 5 gives you single characters 'F', '5', '3'. parseInt('F', 16) returns 15, not 255. Always expand shorthand first.

Treating Alpha as a Percentage

In an 8-digit HEX, 80 doesn't mean 80%. It means decimal 128 out of 255, which is ~50.2% opacity. CC = 204/255 = ~80%. Use the lookup table above when in doubt.

Not Clamping When Writing Back to HEX

If you darken a color by subtracting from each channel, you can underflow below 0 or overflow above 255. (-5).toString(16) in JavaScript returns '-5', not a valid HEX byte. Always clamp: Math.max(0, Math.min(255, value)).

Zero-Padding Output

(14).toString(16) returns 'e', not '0e'. When building a HEX string from RGB values, always pad to two digits: n.toString(16).padStart(2, '0'). Otherwise #0e0000 becomes #e00, which expands to #EE0000 — a completely different color.

Frequently Asked Questions

How do you convert HEX to RGB?

Split the 6-digit HEX code into three 2-character pairs and convert each from base-16 to base-10. For #FF5733: FF = 255, 57 = 87, 33 = 51 → rgb(255, 87, 51). In JavaScript: parseInt(hex.slice(1,3), 16) for red, parseInt(hex.slice(3,5), 16) for green, parseInt(hex.slice(5,7), 16) for blue.

What is the difference between HEX and RGB colors?

HEX and RGB represent identical colors in different notations. HEX uses hexadecimal encoding — compact and standard in design tools. RGB uses decimal integers 0–255 per channel — easier for programmatic manipulation. The browser converts both to the same internal representation. #FF5733 and rgb(255, 87, 51) render identically.

How do you convert 3-digit HEX shorthand to RGB?

Expand by doubling each digit before parsing: #F53 → #FF5533. Then convert as normal: FF=255, 55=85, 33=51 → rgb(255, 85, 51). A common mistake is padding with zeros (#F530) — the CSS spec defines it as digit duplication, not zero-padding.

What does the alpha channel mean in 8-digit HEX codes?

The last two hex digits in an 8-digit code are the alpha (opacity) channel. The range is 00–FF (0–255), divided by 255 to get a 0.0–1.0 fraction. #FF573380 → 0x80 = 128 → 128/255 ≈ 0.502 → ~50% opacity → rgba(255, 87, 51, 0.502). Note: 80 means ~50%, not 80%.

Is there a performance difference between HEX and RGB in CSS?

No. According to MDN Web Docs, the browser's CSS parser normalizes all color formats to the same internal sRGB representation. Any parsing difference is microseconds. Choose based on readability and tooling — not performance.

How do you convert HEX to RGBA with a custom opacity?

Parse the HEX to get R, G, B values, then supply the opacity separately: function hexToRgba(hex, alpha) { const r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16); return `rgba(${r},${g},${b},${alpha})`; } — hexToRgba("#FF5733", 0.5) → "rgba(255, 87, 51, 0.5)".

Why does Figma export colors as HEX but CSS uses rgb()?

Figma uses HEX for compactness (#4F46E5 is 7 chars vs 18 for rgb(79, 70, 229)). CSS supports both equally. When you need programmatic manipulation — tweening, darkening, mixing — RGB integers are easier to work with mathematically. Most developers convert at the API boundary.

Related Developer Tools

Convert Any Color Instantly

Paste a HEX code and get RGB, HSL, OKLCH, and CSS output instantly. Live preview, copy-to-clipboard, no signup required.

Open Color Converter →