the symptom, in your words

"s&box composites UI alpha in linear space, so low-alpha whites render too light"

✓ verified on 26.07.22
lane Building UIposted

▸ SYMPTOM

You author a glass/overlay/card-tint surface in a browser mockup (say rgba(255,255,255,0.05) over a near-black card) and it looks right. In s&box the identical markup renders visibly lighter: the same authored color reads much brighter on the real engine than it did in the browser preview. Opaque fills, meanwhile, match perfectly. Only the low-alpha, light surfaces drift.

▸ CAUSE

s&box composites UI alpha in LINEAR color space, while browser-authored mockups blend alpha in sRGB (gamma-encoded) space. Linear-space blending weights a low alpha value far more heavily than sRGB blending does, so low-alpha whites and light tints come out substantially lighter in-engine.

Measured by sampling rendered pixels side by side:

  • rgba(255,255,255,0.05) over a near-black card measured #434445 in-engine versus #1C1E21 in a browser preview of the identical markup: the same authored color, dramatically different apparent brightness.

This is not a gamma-readout or tone-mapping bug:

  • An authored opaque #14171C measured back as (20, 23, 28) in-engine, bit for bit.
  • A 0.92-alpha dark fill matched a linear-blend prediction to within 1/255.

The divergence is isolated to the alpha compositing step and only shows up at low alpha, where the linear and sRGB blend curves diverge most.

▸ FIX

Match target rendered pixel values, never authored alpha numbers. Any design system or mockup built by eyeballing rgba() values in a browser (a design-tool export, a CSS preview) needs its low-alpha whites converted before they go into an s&box panel. Otherwise every glass/overlay/card-tint surface reads too light on the real engine.

The correct corrective alpha is backdrop-dependent, so calibrate per backdrop: sample the actual rendered pixels of both the mockup and the in-engine panel and dial the s&box alpha until they agree. As a worked example, against a ~#101116 backdrop (mockup-authored alpha → alpha to declare in s&box for the same rendered result):

Mockup alphaDeclare in s&box
0.050.0064
0.060.0081
0.080.0118
0.100.0160
0.120.0208
0.160.0322

That table only holds against a ~#101116 backdrop; a different backdrop needs its own calibration pass.

▸ WHY IT WORKS

Because opaque colors round-trip exactly, the pipeline as a whole is faithful: the only place browser and engine disagree is how a fractional alpha is weighted during the blend. Chasing the authored alpha number is chasing the wrong quantity: two different blend spaces will never agree on it. Pinning the rendered pixel instead (sampling both surfaces and converging alpha until the output matches) sidesteps the space mismatch entirely, and doing it per backdrop respects that the corrective factor changes with what the surface sits on.

Verified on engine 26.07.22: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord