How a Computer Stores a Colour — And Why 128 Is Not Half Brightness

A colour is three numbers. That much is well known. What is far less known is that those numbers do not describe brightness in any straightforward way — 128 emits roughly a fifth of the light of 255, not half. This single fact explains why resized images come out dark, why gradients band, and why blending two colours the obvious way produces the wrong answer.

The core surprise

sRGB values are gamma-encoded. The stored number is raised to approximately the power 2.2 before it becomes light. So rgb(128,128,128) is about 21% as bright as white, not 50%. Almost every "why does this look wrong" question about colour traces back to this.

Bit depth

Each channel is stored with a fixed number of bits, and that number sets how many distinct steps exist between off and full.

Bits per channelStepsTotal coloursUsed by
128Very early hardware
5–632–6465,536Legacy "high colour"
825616,777,216The standard everywhere
101,0241.07 billionHDR, professional displays
124,09668.7 billionCinema, Dolby Vision
1665,5362.8 × 1014Editing intermediates
32 floatContinuousRendering, HDR compositing

Eight bits became standard because 16.7 million colours comfortably exceeds what the eye can distinguish as individual colours. But that is the wrong test. The eye is exceptionally good at spotting boundaries between adjacent shades, and in a smooth gradient — a clear sky, a soft shadow — consecutive 8-bit steps can be visible as distinct bands.

This is why 10-bit matters for HDR content. The extra bits are not for more colours in the abstract; they are for finer steps between neighbouring ones.

💡 Dithering hides banding

Adding a small amount of carefully structured noise breaks up the hard boundary between bands. The eye averages the noise and perceives a smooth transition where none exists at the bit level.

This is why a well-exported 8-bit gradient can look smoother than a naive one — the exporter dithered it. In CSS, adding a faint noise texture over a large gradient does the same thing, and a 1KB tiled PNG often solves banding that no amount of colour tweaking will.

Gamma encoding: the part everyone misses

If 255 is full brightness, it feels obvious that 128 should be half. It is not, and the reason is a genuinely good design decision.

Human vision is not linear

The eye is far more sensitive to differences among dark tones than among bright ones. Double the light coming from a very dark surface and the change is obvious. Double the light from an already bright surface and it is barely noticeable.

So storing brightness linearly wastes precision. Most of the 256 available steps would land in bright regions where the eye cannot tell them apart, while dark regions — where it easily can — would get too few.

The fix

sRGB stores values in a perceptual space rather than a physical one. The relationship between the stored number and emitted light is approximately:

light = (value / 255) ^ 2.2 // Which gives: value 255100% light value 18650% light ← the real midpoint value 12821.6% light ← not half value 645.1% light value 00% light

The steps are packed closely in the dark end and spread widely in the bright end, matching the eye's sensitivity. It is an excellent use of eight bits — and it means the numbers cannot be treated as quantities of light.

🚨 Why averaging sRGB values is wrong

Averaging a black pixel and a white pixel should give a result emitting 50% of the light. Averaging the stored values gives (0 + 255) / 2 = 128, which emits 21.6%. The result is far too dark.

This affects every operation that combines pixels: resizing, blurring, alpha blending, gradients, and antialiasing. Done naively in sRGB, all of them come out too dark.

The demonstration

The classic test is a fine checkerboard of pure black and pure white pixels. Physically it emits 50% light, so viewed from a distance it should match a 50% grey surface.

It does not match #808080. It matches approximately #BABABA — value 186 — because that is the sRGB value that emits half the light. If your software's "50% grey" and a black-and-white checkerboard do not match at a distance, you have just observed gamma encoding directly.

Doing it correctly

// Convert sRGB to linear light, work, convert back function toLinear(c) { c = c / 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); } function toSRGB(c) { c = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055; return Math.round(c * 255); } // Correct midpoint of black and white toSRGB((toLinear(0) + toLinear(255)) / 2); // 188 // Naive version (0 + 255) / 2; // 128 — far too dark

Note that the real sRGB transfer function is not a plain power of 2.2 — it has a small linear segment near black to avoid an infinite slope at zero. The 2.2 figure is a close approximation, and the piecewise version above is the actual specification.

✅ Where this shows up in practice

  • Image resizing — the most common case. Photoshop has a "Convert to linear when resizing" preference; many tools have no equivalent.
  • CSS gradients — browsers interpolate in sRGB by default, which is why a black-to-white gradient looks dark in the middle. linear-gradient(in oklab, …) fixes it.
  • Alpha compositing — a semi-transparent white over black gives a darker result than expected.
  • Blur and glow effects — bright areas do not bloom correctly.
  • 3D rendering — lighting must be computed in linear space and converted for display at the end.

Colour spaces

Bit depth says how many steps exist. A colour space says what those steps actually mean — which physical colours sit at each end.

SpaceGamut coverageUsed for
sRGBBaselineThe web, most displays — assume this
Display P3~25% widerModern Apple devices, phones
Adobe RGBWider in greensPrint production
Rec. 709≈ sRGBHD video
Rec. 2020Much widerUHD and HDR video
ProPhoto RGBVery wideEditing masters

The critical point: the same three numbers mean different colours in different spaces. rgb(255,0,0) is "the reddest red this space can express", and in Display P3 that is a visibly more saturated red than in sRGB.

This is why colour profiles exist. A profile tags an image with which space its numbers belong to. Strip the profile and the viewer assumes sRGB — so a Display P3 photo displayed as sRGB looks flat and desaturated, and an sRGB image interpreted as P3 looks oversaturated.

/* CSS can now address wide gamut explicitly */ .vivid { color: rgb(255 0 0); /* sRGB red */ color: color(display-p3 1 0 0); /* a redder red */ } /* Use the wider colour only where it is supported */ @supports (color: color(display-p3 1 0 0)) { .vivid { color: color(display-p3 1 0 0); } }

Alpha and the fringe problem

A fourth channel stores opacity. It is stored one of two ways, and the difference produces a specific visible artefact.

Straight alphaPremultiplied alpha
StoresColour and alpha separatelyColour already multiplied by alpha
50% red(255, 0, 0, 128)(128, 0, 0, 128)
CompositingMultiply every timeSimple addition — faster
Fully transparentColour is undefinedAlways zero
FilteringCan produce fringesCorrect

The fringe problem is worth spelling out because it is a common and puzzling bug. In straight alpha, a fully transparent pixel still holds colour values — and nothing constrains what they are. Many editors leave them white or black.

When the image is scaled or blurred, the filter averages neighbouring pixels including their colour channels, transparent or not. Those invisible white or black values bleed into the visible edge, producing a pale or dark halo around every shape. Premultiplied alpha eliminates it, because a transparent pixel's colour is necessarily zero and contributes nothing.

The notations, and what they are for

NotationExampleGood for
Hex#2563EBCompact, universal
RGBrgb(37 99 235)Programmatic manipulation
HSLhsl(221 83% 53%)Adjusting by hand — but lightness misleads
LABlab(45% 20 -70)Perceptually uniform
OKLCHoklch(0.52 0.19 264)Palettes and interpolation

HSL is intuitive and quietly unreliable, for the same reason as everything else on this page. Its lightness is a naive average of RGB values, so hsl(60 100% 50%) (yellow) appears dramatically brighter than hsl(240 100% 50%) (blue) despite both claiming 50% lightness. A palette built by holding HSL lightness constant will not look consistent.

OKLCH is built on a perceptual model, so equal L values genuinely look equally bright across every hue. That makes it the first notation where you can generate a set of colours by varying hue at fixed lightness and get a result that is actually consistent — including for accessibility, where contrast depends on perceived brightness rather than the numbers.

/* Six hues, genuinely equal perceived lightness */ :root { --c1: oklch(0.65 0.15 30); --c2: oklch(0.65 0.15 90); --c3: oklch(0.65 0.15 150); --c4: oklch(0.65 0.15 210); --c5: oklch(0.65 0.15 270); --c6: oklch(0.65 0.15 330); } /* The same attempt in HSL produces a yellow that glares and a blue that disappears. */

Convert between colour notations

Paste a colour in any format and get hex, RGB, HSL and more — instantly, in your browser.

Open the Color Converter →

Summary

  • sRGB values are gamma-encoded. 128 emits about 21% of the light of 255.
  • The encoding is deliberate — it matches the eye's non-linear sensitivity and uses 8 bits well.
  • Averaging sRGB values is wrong. Convert to linear, combine, convert back.
  • This affects resizing, blurring, blending and gradients — dark results are the signature.
  • 8 bits is enough for colours, not for gradients. Dither, or use 10-bit.
  • The same numbers mean different colours in different spaces. Always tag the profile.
  • Premultiplied alpha prevents edge fringes when scaling transparent images.
  • HSL lightness lies. Use OKLCH for anything where consistency matters.

Frequently Asked Questions

Why is RGB 128 not half as bright as 255?

Because sRGB values are gamma-encoded rather than linear. The stored number is raised to roughly the power 2.2 before becoming light, so 128 out of 255 produces about 21% of the light of 255, not 50%. The encoding exists because human vision is also non-linear, so it distributes the available precision where the eye can use it.

Why do my images look darker after resizing?

Because the resampler averaged gamma-encoded values as if they were linear. Averaging 0 and 255 in sRGB gives 128, which is 21% brightness — but the correct average of no light and full light is 50%. Correct resizing converts to linear light, resamples, then converts back. Many tools do not.

How many colours can a computer display?

With 8 bits per channel, 16,777,216 combinations. That is enough for photographs but not for smooth gradients — a subtle sky can show visible banding because consecutive steps are too far apart in dark regions. 10-bit encoding gives 1.07 billion and largely eliminates it.

What is the difference between sRGB and Display P3?

They are different gamuts — different ranges of reproducible colour. Display P3 covers roughly 25% more, particularly in saturated reds and greens. The same numeric values mean different colours in each, which is why an image tagged with the wrong profile looks either washed out or oversaturated.

What is premultiplied alpha?

Storing colour channels already multiplied by their alpha value. It makes compositing simpler and faster and prevents dark or white fringes around the edges of transparent images, which appear when a transparent pixel's colour value is undefined and gets blended in during scaling.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.