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