---
title: 'Color Representations'
source: 'https://academia.sh/en/courses/css-fundamentals/color-representations'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:18+00:00'
license: 'CC BY-SA 4.0'
---

# Color Representations

Named, hexadecimal, and functional color notations; the conversion between RGB and HSL, and the consequence of color space choice for generating a scale.

The previous topic established the boxes' measure, arrangement, and
boundaries. The resulting page is structured but colorless: every
decision concerned geometry. This topic takes up visual properties,
and the first question is color itself.

A color has four separate notations in CSS, and moving between them
is done by calculation. Choosing the notation is not a style
preference: some notations make **changing** a color more workable
than others, and a design's color scale therefore gets built in a
particular representation.

## Four Notations

**A named color** is a name from a fixed vocabulary: `red`,
`darkslategray`, `transparent`. The vocabulary is fixed and does not
grow; the names do not form a color system, they are a historical
list. It does not get used in design.

**Hexadecimal notation** writes the red, green, and blue components
in base sixteen: `#143a52`. The hexadecimal base defined in the How
Computers Work curriculum finds its counterpart here — each component
is one byte, two hexadecimal digits.

**Functional notation** writes the components explicitly: `rgb(20 58
82)`, `hsl(203 61% 20%)`, `oklch(30% 0.06 240)`. The function name
says which **color space** the numbers get read in.

**Keywords** take their color from context: `currentcolor` gives the
element's `color` value, `transparent` gives fully transparent black.

## The Conversion Between RGB and HSL

RGB and HSL define the same set of colors; their difference is the
coordinate system. RGB uses three light components, HSL uses the hue,
saturation, and lightness axes. The conversion is two-directional and
lossless.

```js
// color.mjs — converts between hex, rgb, and hsl representations
function hexToRgb(hex) {
  let h = hex.replace("#", "");
  if (h.length === 3) h = [...h].map((c) => c + c).join("");
  if (h.length === 4) h = [...h].map((c) => c + c).join("");
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
  return { r, g, b, a };
}

function rgbToHsl({ r, g, b }) {
  const rn = r / 255, gn = g / 255, bn = b / 255;
  const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
  const diff = max - min;
  const l = (max + min) / 2;
  let h = 0, s = 0;
  if (diff !== 0) {
    s = diff / (1 - Math.abs(2 * l - 1));
    if (max === rn) h = 60 * (((gn - bn) / diff) % 6);
    else if (max === gn) h = 60 * ((bn - rn) / diff + 2);
    else h = 60 * ((rn - gn) / diff + 4);
    if (h < 0) h += 360;
  }
  return { h: Math.round(h), s: +(s * 100).toFixed(1), l: +(l * 100).toFixed(1) };
}

const colors = ["#143a52", "#8a1c1c", "#f5f7f8", "#fff", "#1c2733", "#8a5b00", "#d5dbe0"];
console.log("hex         rgb()                  hsl()");
for (const hex of colors) {
  const c = hexToRgb(hex);
  const h = rgbToHsl(c);
  console.log(
    `${hex.padEnd(11)} rgb(${c.r} ${c.g} ${c.b})`.padEnd(34) +
    `hsl(${h.h} ${h.s}% ${h.l}%)`
  );
}

console.log("\n--- expansion of short forms ---");
for (const hex of ["#fff", "#0a3", "#143a52cc"]) {
  const c = hexToRgb(hex);
  console.log(`${hex.padEnd(11)} -> rgb(${c.r} ${c.g} ${c.b}) alpha=${c.a.toFixed(3)}`);
}

console.log("\n--- generating a scale by varying one hue's lightness ---");
const base = rgbToHsl(hexToRgb("#143a52"));
for (const l of [12, 24, 36, 50, 72, 90, 96]) {
  console.log(`hsl(${base.h} ${base.s}% ${l}%)`);
}
```

```
hex         rgb()                  hsl()
#143a52     rgb(20 58 82)         hsl(203 60.8% 20%)
#8a1c1c     rgb(138 28 28)        hsl(0 66.3% 32.5%)
#f5f7f8     rgb(245 247 248)      hsl(200 17.6% 96.7%)
#fff        rgb(255 255 255)      hsl(0 0% 100%)
#1c2733     rgb(28 39 51)         hsl(211 29.1% 15.5%)
#8a5b00     rgb(138 91 0)         hsl(40 100% 27.1%)
#d5dbe0     rgb(213 219 224)      hsl(207 15.1% 85.7%)

--- expansion of short forms ---
#fff        -> rgb(255 255 255) alpha=1.000
#0a3        -> rgb(0 170 51) alpha=1.000
#143a52cc   -> rgb(20 58 82) alpha=0.800

--- generating a scale by varying one hue's lightness ---
hsl(203 60.8% 12%)
hsl(203 60.8% 24%)
hsl(203 60.8% 36%)
hsl(203 60.8% 50%)
hsl(203 60.8% 72%)
hsl(203 60.8% 90%)
hsl(203 60.8% 96%)
```

## Hexadecimal Notation's Shorthands

The output's second block unpacks three shorthands.

In the three-digit writing, every digit gets **doubled**: `#fff`
means `#ffffff`, `#0a3` means `#00aa33`. This means the number of
colors that can get written with three digits is 4096 — a small
subset of the full set. `#0a3` is not an approximation, it is exactly
`#00aa33`.

In the eight-digit writing, the last two digits carry the **alpha
channel**. In the writing `#143a52cc`, `cc`'s hexadecimal value is
204, and dividing it by 255 gives 0.8. The alpha channel gets taken up
in the next lesson.

## Color Space Choice Is a Scale Decision

In the output's first block, the color `#143a52` gets written as
`hsl(203 60.8% 20%)`. The two representations give the same color;
their difference shows up once change is needed.

If a lighter tone of this color needs producing, in hexadecimal
notation all three components have to get calculated separately, and
the hue can drift. In HSL notation, only the third number gets
changed; hue and saturation stay fixed. The output's third block shows
this: a seven-step scale got produced from a single hue, by changing
only the lightness value.

A design's color scale therefore gets built in a hue-axis
representation. HSL has a known limitation: equal lightness
differences do not correspond to equal **perceived** differences. At
the same lightness value, yellow looks noticeably brighter than blue.
Perceptually more uniform spaces like `oklch()` are defined to reduce
this problem, and have the same three axes: lightness, chroma, hue.

Whether a representation is recognized by implementations can get
tested:

```css
.warning { background-color: #8a5b00; }

@supports (color: oklch(50% 0.1 40)) {
  .warning { background-color: oklch(45% 0.12 75); }
}
```

The block gets ignored if the condition is not met, and the first
declaration stays in place. The "an invalid declaration gets dropped"
rule seen in previous lessons would give the same result; writing
`@supports` makes the intent explicit.

## Color Variables

A color scale should get defined once and called by name everywhere.
The tool for this is custom properties:

```css
:root {
  --hue: 203;
  --brand-dark:  hsl(var(--hue) 61% 20%);
  --brand-mid:   hsl(var(--hue) 61% 36%);
  --brand-light: hsl(var(--hue) 61% 90%);
  --surface:     hsl(var(--hue) 18% 97%);
  --text:        hsl(211 29% 16%);
  --warning:     hsl(0 66% 33%);
}
```

Custom properties get defined with a double-hyphen prefix and get
read with `var()`. They are inherited properties: a value defined on
the root element can get read throughout the whole subtree. This also
makes it possible to change the value for a subtree by redefining it
inside a component.

This writing has a side effect: because `--hue` is kept in one place,
the whole scale can get shifted by changing a single number.

One warning is needed: a custom property's value does not get
validated during parsing. If something invalid gets written into
`--brand-dark`'s value, the error shows up where the value gets used,
and that declaration drops.

## The Station Page's Color Scale

```css
/* station.css — step 16: color scale */
:root {
  --hue: 203;
  --brand-dark: hsl(var(--hue) 61% 20%);
  --brand-light: hsl(var(--hue) 61% 90%);
  --surface:      hsl(var(--hue) 18% 97%);
  --line:         hsl(207 15% 86%);
  --text:         hsl(211 29% 16%);
  --text-muted:   hsl(211 12% 45%);
  --warning:      hsl(0 66% 33%);
  --caution:      hsl(40 100% 27%);
}

body { color: var(--text); }
.masthead { background-color: var(--surface); border-block-end-color: var(--brand-dark); }
.measurement-table th, .measurement-table td { border-block-end-color: var(--line); }
.missing { color: var(--warning); }
.source-note { color: var(--text-muted); }
```

The names in the scale name **function**, not color: `--warning`,
`--line`, `--text-muted`. This is the naming principle from selectors
applied to colors. A variable named `--red` starts lying once the
warning color gets changed to orange.

The `currentcolor` keyword is useful together with this scale: writing
`border-color: currentcolor` on a border ties the border to the text
color, and once the `.missing` class turns the text red, the border
changes along with it. A single declaration keeps two properties
synchronized.

## Summary

- A color gets written in four forms: from the named vocabulary, in
  hexadecimal notation, in functional notation, and with keywords that
  take color from context.
- In hexadecimal notation, the three-digit form doubles every digit;
  in the eight-digit form, the last two digits carry the alpha
  channel.
- RGB and HSL define the same set of colors with different
  coordinates; the conversion is two-directional, and the choice of
  representation determines how workable changing a color is.
- A color scale gets built in a hue-axis representation: a consistent
  sequence of steps gets produced by changing a single hue's
  lightness.
- Color variables name function, not color; they get defined as
  inherited custom properties and can get changed locally by getting
  redefined in a subtree.

## Next Step

This lesson treated colors as fully opaque; the alpha channel seen in
the eight-digit writing went by unexplained. What color results when
a transparent color overlaps another, and how does that color's
contrast with text get calculated? The next lesson takes up alpha
blending and the WCAG contrast ratio, numerically.
