← Back to blogBuild Log

Why Every Sprint Engine Studio Theme Is 6-Bit: Designing for PWM and Dithering Sensitivity

Some developers physically can't use a screen for long because of PWM flicker and temporal dithering. We can't fix their hardware, but we made sure our software is never the reason a screen flickers — so every SE Studio theme is built on a 6-bit color grid.

Sprint Engine Studio6 min read

There is a quiet group of developers who cannot use certain screens for long without headaches, eye strain, nausea, or worse. It is not a preference. For some people a particular laptop panel is simply unusable, and they spend real money and research trying to find hardware that does not make them ill.

If you have never had to think about this, it sounds niche. Spend ten minutes in communities like r/PWM_Sensitive and the screen-sensitivity forums and it stops sounding niche. People trade detailed notes on which displays flicker, which GPUs force dithering, and which OS updates quietly made a machine they relied on unusable. We build a tool developers stare at all day. We wanted to make sure SE Studio was never the thing that pushed someone over the edge.

Two different flickers

It helps to separate the two things people lump together. PWM, or pulse-width modulation, is how many screens dim their backlight: instead of lowering brightness smoothly, they switch the backlight on and off very fast. At lower brightness the off periods get longer, and for sensitive people that flicker is exhausting even when they cannot consciously see it.

Temporal dithering is different and lives closer to software. A lot of panels are natively 6-bit per channel — 64 levels of red, green, and blue — and fake the look of 8-bit by rapidly alternating between two adjacent colors every frame. The eye averages the flicker into the in-between shade. This trick is called FRC, frame rate control, and GPUs apply their own temporal dithering on top of it. The result is the same: pixels that are quietly strobing between two values, frame after frame.

PWM is the backlight. Dithering is the color. We have no control over a user's backlight. We have a lot of control over which colors we ask their screen to draw.

Dithering happens when a color is off the grid

The key insight is that a panel only needs to dither when you ask for a color it cannot display directly. If a flat UI surface is set to a color that lands exactly on the panel's native grid, there is nothing to approximate. The pixel can just sit there, steady. The flicker only appears for the in-between values.

Why an off-grid color flickers

Off-grid colorrgb(58, 82, 214)needs ditheringframe 1frame 2frame 3frame 4flickersevery frame6-bit-alignedrgb(56, 80, 212)displayed as-issteadyno ditheringColor difference exaggerated for illustration.
An off-grid color is faked by alternating two neighbors every frame. A color already on the grid is just drawn — no flicker.

So we put every theme color on the grid

Six bits per channel means 64 levels, and 256 divided by 64 is 4. So a color is 6-bit-native when every channel is a multiple of 4 — when the low two bits are zero. We made that a hard rule for every theme token in SE Studio. A color that is exactly representable in 6 bits is shown without temporal approximation on the 6-bit panels that rely on FRC, and stays exact on 8-bit panels too. It is the safe subset for both.

css
/* Every channel is a multiple of 4 (low two bits zero), so the
   color lands on the panel's 6-bit grid and never needs temporal
   dithering to be displayed. */
--color-paper:  #f8f8f4; /* rgb(248, 248, 244) */
--color-ink:    #1c1c18; /* rgb( 28,  28,  24) */
--color-accent: #3850d4; /* rgb( 56,  80, 212) */

Doing this by hand is a great way to ship an off-grid color by accident, so it is not a hand process. Theme colors are snapped to the grid when a theme is built, and a check in CI fails the build if any token slips off it.

typescript
// 64 levels instead of 256: snap each channel to the 6-bit grid.
const snap6bit = (c: number) => Math.round(c / 4) * 4

// CI guard: no theme token may sit off the grid.
for (const [name, [r, g, b]] of themeTokens) {
  if (r % 4 || g % 4 || b % 4) {
    throw new Error(`${name} is not 6-bit aligned`)
  }
}

The visual cost of this is essentially nothing. Snapping a channel moves it by at most two steps out of 255 — a shift you cannot see in a flat fill. The accessibility benefit is real: the large, calm surfaces a developer looks at for hours are drawn from steady colors instead of strobing ones.

What this does and does not fix

We want to be honest about the boundary, because over-claiming helps no one in these communities. A 6-bit theme removes the reason our flat UI surfaces would trigger panel FRC. It does not, and cannot, do everything.

  • It does not fix PWM. Backlight flicker is hardware; no app can change it.
  • It cannot override a GPU or OS that is configured to force temporal dithering globally. It removes our contribution to it, not the system's.
  • Photos, gradients, and antialiased edges still produce off-grid colors, so we keep theme surfaces flat and avoid large gradients in the UI.
  • Where an effect could dither — soft shadows, blur, noise textures — it should be reducible or off, not load-bearing.

Put together, the rule is simple: the parts of the interface a person stares at the longest are built from colors their screen can render without flicker, and the parts that cannot make that promise are kept small and optional.

Why we bothered

Most users will never notice this, and that is fine. It costs us a build step and a lint rule. For the people who do notice — the ones who have had to abandon tools and laptops because the screen made them sick — it is the difference between software they can use all day and software they cannot use at all.

We can't fix your screen's backlight. We can make sure our software is never the reason it flickers.

If you are sensitive to PWM or dithering and you still hit problems in SE Studio, tell us. The communities below have done years of careful, unglamorous documentation on this, and we would rather learn from them than guess.

Sources and Further Reading