Skip to content

fix(editor): Blueprint icons paint under page zoom in Chrome 153 - #41

Closed
pythonlearner1025 wants to merge 1 commit into
mainfrom
fix/zoom-icons
Closed

pythonlearner1025 wants to merge 1 commit into
mainfrom
fix/zoom-icons

Conversation

@pythonlearner1025

@pythonlearner1025 pythonlearner1025 commented Sep 16, 2026 •

Copy link
Copy Markdown
Member

What part this touches

The editor is a React app. Blueprint is its interface library. Blueprint draws each icon as an
inline svg inside a span.bp5-icon.

The browser's own default stylesheet is called the user agent stylesheet. It puts
overflow: hidden on every inline svg. That clip is what this change lifts, for Blueprint icons
only.

Page zoom in Chrome 153 is a css zoom on the root element. It is not a repaint at a different
scale. Layout runs in zoomed units, and the clip is computed there too.

This change touches one file, packages/editor/src/renderer.scss, the editor stylesheet. It adds
one rule. There is no JavaScript and no zoom detection.

The bug

The owner runs the editor in Google Chrome 153 at 75% page zoom, on a Retina display. Every
Blueprint icon was gone. That is the navbar folder, scene and save icons. It is also both floating
viewport toolbars, the panel flaps and the tab kind icon. The hierarchy icons went too. The two
active toolbar buttons showed as blank green and blue squares.

The icon was never missing from the page. At 75% zoom I read each one back:

  • The svg and its path are in the dom.
  • The svg has a real rect, 10.5 by 10.5 css pixels for a 14 pixel navbar icon.
  • document.elementFromPoint on the middle of that rect returns the icon. It is still clickable.
  • Its painted pixels are zero.

The count of lit pixels in a crop around the navbar folder-close icon, in the owner's Chrome:

zoom 1.00   546 lit pixels
zoom 0.75     0 lit pixels

It is not only 75%, and it is not only Retina. I swept every zoom step in Chrome's own menu. I ran
the sweep at device pixel ratio 2 and 1. Every step from 25% to 75% loses the icons. From 80% up
they paint, though at some steps only part of an icon paints.

The cause is the clip. These icons are small inline svg elements. Under css zoom, Chrome 153
computes their user agent overflow: hidden as an empty clip. Nothing inside them reaches the
screen.

Blueprint's own stylesheet already carries a matching workaround for one case,
.bp5-non-ideal-state-visual .bp5-icon svg { overflow: visible }.

Two other clues fit the same cause. The tree carets keep painting at 75%, which is why the
hierarchy looked half broken rather than empty. The 32 pixel folder icons in the Files panel keep
painting too. They are large enough that the clip does not collapse.

The fix

Lift the clip on Blueprint icon svgs, once, in packages/editor/src/renderer.scss:

.bp5-icon > svg {
  overflow: visible;
}

The comment above the rule names the browser, the mechanism and why it is free at 100%.

No JavaScript. No zoom detection. No per component override. No !important, because an author
rule already beats the user agent rule.

The risk trade

Lifting a clip can only cost one thing. An icon that draws outside its own viewBox would now
paint over its neighbour, instead of being cut off.

I measured that. I read all 1360 icon paths the installed @blueprintjs/icons ships. That is 680
icons, in both the 16 and the 20 pixel size. I measured each drawn bounding box against its
viewBox in Chrome. 230 of them leave the box. The largest excursion is 0.08 user units out of
a 16 unit box. On a 14 pixel icon at device pixel ratio 2 that is 0.14 of one device pixel.
Nothing can visibly bleed.

The measurement is in bbox.json in the evidence folder, and bbox.mjs reproduces it.

I rejected the other candidate that worked, .bp5-icon > svg { width: 100%; height: 100% }. It
lit the icon again, but it changes layout. The shipped stylesheet already sets a size on that
selector in five places. A size rule would have to win against all of them. It would also be a
real visual change at 100%. overflow is not a layout property, so it changes nothing that is
already correct.

I rejected a zoom listener that swaps styles. There is nothing to detect. The rule is correct at
every zoom.

Layer promotion, will-change, contain, isolation, display changes, vector-effect,
image-rendering and a tiny transform were all tried first. None of them makes the icon paint.

Tests

No test was added. The editor package has no test runner, by decision. A test that guards this
needs a browser at a zoom, a dev server and a pixel read. The repo rule is to keep the suites
small. So the proof is the headless run below, and its scripts stay in the evidence folder.

All runs are headless Google Chrome 153.0.8010.48, the owner's own binary. The viewport was 1512
by 982 at device pixel ratio 2. The project was /Users/minjunes/games/terminator-next. The dev
server ran with CI=1 and --no-open. Nothing took focus. I confirmed by sha256 that the editor
bundles the server sent were the ones this branch built.

I measured twice, once on a build of origin/main and once on a build of this branch.
measure.mjs counts the lit pixels of three named icons. It then counts every icon on the page,
from one full screenshot.

Icons painted, out of the icons visible in their panel:

zoom before after
1.00 63 of 63 63 of 63
0.75 38 of 69 69 of 69
0.90 64 of 66 66 of 66
1.25 49 of 49 49 of 49

The three named icons, lit pixels, before to after:

zoom navbar folder-close a toolbar icon a hierarchy icon
1.00 546 to 546 440 to 440 300 to 300
0.75 0 to 315 0 to 237 0 to 180
0.90 452 to 452 344 to 348 234 to 234
1.25 815 to 835 731 to 731 483 to 483

What fails without the fix is the 0.75 row. Three of three named icons sit at zero. 31 of 69 icons
on the page are blank.

The rule is invisible at 100%. Across all 63 icons at zoom 1, the largest change in pixel count
between the two builds is 0. The navbar strip of the two screenshots is byte for byte identical,
0 differing subpixels out of 907,200.

Screenshots, all looked at before writing this:

  • before-full-zoom0_75.png, the owner's report reproduced. Text with no icons.
  • after-full-zoom0_75.png, the same page on this branch. Every icon draws, in line with its
    label, with no overlap and no change of spacing.
  • navbar-before-zoom075.png and navbar-after-zoom075.png, the navbar alone, side by side.

Suites, from the worktree, all pass:

  • npm run typecheck, engine, editor and kite3d.
  • npm run lint, engine, editor and kite3d.
  • npm test -w packages/kite3d, 3 files, 6 tests.
  • npm run test:scripts, 2 tests.

Edges I tested, and what happens off the happy path:

  • Every zoom step in Chrome's menu, 25% to 500%, at device pixel ratio 2 and 1. Without the rule,
    25%, 33%, 50%, 67% and 75% lose all three icons at both ratios. With the rule, all three paint
    at every step.
  • Partial clipping above 100%. A few steps painted only part of an icon before. A toolbar icon at
    175% went from 1556 to 3032 lit pixels. The rule completes those too.
  • Zoom above 200%. The hierarchy icon leaves the viewport. It is reported as off screen rather
    than measured. The other two keep painting.
  • Causation, proved in one page. I switched the built rule off and on through the cssom, and
    changed nothing else. The icons go blank and come back.
  • Icons scrolled out of the Files panel stay blank at every zoom, before and after. They sit
    outside their own panel's scroll box, which is not this bug. The census does not count them.

Deploy

The artifact is the editor stylesheet, inside the editor bundle in @kite3d/editor. kite3d dev
serves that bundle, and the next kite3d npm release ships it. There is no server change and no
CLI change.

npm ci
npm run build
npm run typecheck
npm run lint
npm test -w packages/kite3d
npm run test:scripts

Roll back by reverting this commit and rebuilding, or by installing the previous version:

npm install kite3d@0.21.0-alpha.2

Chrome 153 painted nothing inside a Blueprint icon below 80% page zoom. The
icon stayed in the dom, laid out and clickable. Its pixels never appeared.

The owner runs the editor at 75%. There the navbar icons, both viewport
toolbars, the panel flaps, the tab kind icon and the hierarchy icons were gone.

Page zoom is a css zoom on the root element. The user agent stylesheet clips an
inline svg with overflow: hidden. Under that zoom Chrome computes the clip as
empty for these small icons. One rule lifts the clip.

Blueprint draws inside the viewBox, so the rule changes nothing at 100%. I
measured all 1360 icon paths the package ships. The largest excursion past the
viewBox is 0.08 of a 16 unit box. That is a fraction of one device pixel.

Verified headless in Google Chrome 153.0.8010.48 against the terminator
project. I built this branch and origin/main, and measured both:

- Icons painted at zoom 0.75: 38 of 69 before, 69 of 69 after.
- Icons painted at zoom 0.9: 64 of 66 before, 66 of 66 after.
- Navbar folder-close, a toolbar icon and a hierarchy icon at zoom 0.75:
  0, 0 and 0 lit pixels before; 315, 237 and 180 after.
- At zoom 1 all 63 icons keep the same pixel count in both builds. The navbar
  strip of the two screenshots is byte for byte identical.
- Chrome zoom steps 25%, 33%, 50%, 67% and 75% lose the icons without the rule.
  They keep them with it, at device pixel ratio 1 and 2.

npm run typecheck, npm run lint, npm test -w packages/kite3d (6 tests) and
npm run test:scripts (2 tests) all pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pythonlearner1025
pythonlearner1025 deleted the fix/zoom-icons branch September 16, 2026 21:03
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.

1 participant