Skip to content

fix(#305): colorblind mode no longer un-fixes the whole page - #306

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/305-colorblind-fixed-trap
Jul 17, 2026
Merged

fix(#305): colorblind mode no longer un-fixes the whole page#306
TortoiseWolfe merged 1 commit into
mainfrom
fix/305-colorblind-fixed-trap

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #305.

The bug

useColorblindMode set body.style.filter. A filter other than none makes its element a
containing block for position: fixed descendants — the CSS Filter Effects spec exempts only
the root element, and <body> is not the root.

So enabling the accessibility feature silently anchored every fixed element to <body> and made
it scroll with the document. Already affected on main:

Measured, not theorised

On /chatt scrolled to y=150, a position:fixed; top:64; bottom:0 probe:

body filter probe top probe bottom
none 64 630
url(#protanopia) -86 721

It hides wherever body height happens to equal the viewport (body's padding box is the viewport
then, so there's no offset to diverge). That coincidence is what let it live.

The fix

Move the filter to the root element. Same page, same scroll:

where probe top probe bottom
no filter 64 630
<html> 64 630
<body> -86 721

The root filter still paints — verified deterministically: a pure-red swatch renders differently
under protanopia from <html> (computed style is not proof of paint).

Also drops --colorblind-filter, which was set on the root and consumed by nothing.

Why this shipped, and what stops it recurring

All 8 unit assertions were expect(document.body.style.filter).toBe('url(#tritanopia)') — they
check the property is SET. A containing-block change is a layout fact and jsdom has no
layout, so they passed for the bug's entire life. Fourth costume of this repo's recorded trap:
green assertion, wrong screen.

tests/e2e/colorblind-fixed.spec.ts measures real geometry in a real browser:

  • Verified RED on the old code — failed at expect(result.scrolled).toBe(result.unscrolled)
    with Expected: 64, Received: -86 — i.e. it fails on the bug it names.
  • Its precondition deliberately polls for the filter being applied anywhere (<html> OR
    <body>), so on the buggy code it fails on the geometry rather than tautologically on
    "you didn't move it".
  • It asserts the test page actually scrolls, so it can't pass vacuously on a page where the bug
    is invisible.

Verification

`useColorblindMode` set `body.style.filter`. A filter other than `none` makes
its element a containing block for position:fixed descendants -- the CSS
Filter Effects spec exempts only the ROOT element, and <body> is not the root.
So enabling the accessibility feature silently anchored every fixed element to
<body> and made it scroll with the document: the atlas tour caption (which
#297 moved to `fixed` precisely to rescue it), the diorama shell, the cookie
banner, /messages.

Measured on /chatt scrolled to y=150, a `fixed top:64;bottom:0` probe:

  filter on <body>  ->  top -86, bottom 721   (scrolls with the page)
  filter on <html>  ->  top  64, bottom 630   (identical to no filter)

The root filter still paints: a pure-red swatch renders differently under
protanopia from <html>, so the move costs nothing visually.

Why this shipped: all 8 unit assertions were `expect(document.body.style
.filter).toBe('url(#tritanopia)')` -- they check the property is SET. A
containing-block change is a LAYOUT fact and jsdom has no layout, so they
passed for the bug's entire life. The new e2e spec measures real geometry in a
real browser; verified RED on the old code (expected 64, received -86) before
being made to pass, and its precondition deliberately polls for the filter
being applied ANYWHERE so it fails on the bug rather than on "you didn't move
it".

Also drops `--colorblind-filter`, set on the root and consumed by nothing.

Closes #305
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Colorblind mode breaks position:fixed site-wide (filter on <body> creates a containing block)

2 participants