"s&box composites UI alpha in linear space, so low-alpha whites render too light"
▸ 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#434445in-engine versus#1C1E21in 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
#14171Cmeasured 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 alpha | Declare in s&box |
|---|---|
| 0.05 | 0.0064 |
| 0.06 | 0.0081 |
| 0.08 | 0.0118 |
| 0.10 | 0.0160 |
| 0.12 | 0.0208 |
| 0.16 | 0.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.
- Published