Skip to content

fix(@wterm/dom): pixel-snap upper-block gradients to prevent sub-pixel misalignment#81

Open
codevibesmatter wants to merge 1 commit into
vercel-labs:mainfrom
codevibesmatter:fix/dom-upper-block-alignment
Open

fix(@wterm/dom): pixel-snap upper-block gradients to prevent sub-pixel misalignment#81
codevibesmatter wants to merge 1 commit into
vercel-labs:mainfrom
codevibesmatter:fix/dom-upper-block-alignment

Conversation

@codevibesmatter
Copy link
Copy Markdown

Problem

Claude Code's TUI draws horizontal rules with a run of U+2594 ( UPPER ONE EIGHTH BLOCK) cells. The DOM renderer paints these via linear-gradient(<fg> 12.5%, <bg> 12.5%) set as inline background on a .term-block span (packages/@wterm/dom/src/renderer.ts:165-166).

At the canonical row-height of 17px, 12.5% resolves to 2.125px — a sub-pixel boundary the browser rounds inconsistently from cell to cell. The result is a visible per-cell jog along the horizontal rule, and 1px gaps or overlaps against the row immediately below (usually box-drawing or empty cells).

I noticed this running Claude Code inside a tmux pane wrapped by @wterm/react — every /login, /diff, panel screen, etc. paints a U+2594 horizontal rule that's visibly jagged.

Compare:

Renderer Result
@xterm/xterm (canvas) smooth horizontal rule
@wterm/dom (today) per-cell jog, 1px misalignment with adjacent rows
@wterm/dom (this PR) smooth horizontal rule

Fix

Route every percentage-based vertical-axis block gradient stop through round(calc(var(--term-row-height) * N/8), 1px) so the boundary lands on a whole device pixel. All U+2580..U+2587 and U+2594 stops are converted in one pass — they share the same sub-pixel hazard, U+2594 is just the one Claude Code surfaces most often.

Horizontal-axis gradients (U+2589..U+258F, U+2590, U+2595) and quadrant codepoints are left unchanged. Their gradient line is the cell width (1ch ≈ font-advance), and the rounding hazard is far less visible across cells than along a continuous horizontal rule. Worth a separate look later but the user-visible bug is the vertical-axis one.

Tests

packages/@wterm/dom/src/__tests__/upper-block-alignment.test.ts (new file, 2 tests):

  1. Asserts the emitted CSS string for a U+2594 cell does not contain a bare 12.5% stop, AND does contain a pixel-typed length (round(...) / px).
  2. Asserts five adjacent U+2594 cells produce identical background strings — necessary precondition for the rule to render as a continuous line.

The CSS-string assertion is the right level: the bug symptom (the pixel jog) only appears in a real browser, but the bug cause (a sub-pixel gradient stop) is fully visible in emitted CSS, which is what the renderer controls.

Verification

Full CI gauntlet passes locally:

  • pnpm test — 13/13 groups, DOM count 68 → 70
  • pnpm lint — clean (one pre-existing warning in apps/docs/postcss.config.mjs, unrelated)
  • pnpm type-check — 22/22
  • pnpm build — 15/15
  • pnpm test:e2e — 11/11 (validates we didn't regress rendering structure)

Did not touch the Zig core, so the committed WASM is unchanged.

Out of scope

I noticed two related issues while debugging Claude Code rendering that aren't in this PR:

  • Wide-character cursor desync with (U+276F) — looks like it overlaps PR Fix wide Unicode cell rendering #75 from @ctate, happy to wait for that to land.
  • Alt-screen residue when switching out of CSI ?1049 — lives in Zig (src/terminal.zig:481-501 switchScreen), much bigger change.

Happy to file these as separate issues with byte-sequence repros if it helps.


🤖 Drafted with Claude Code

…lignment

Claude Code's TUI draws horizontal rules as a run of U+2594 (UPPER ONE
EIGHTH BLOCK) cells. The DOM renderer painted these via
`linear-gradient(<fg> 12.5%, <bg> 12.5%)` on a `.term-block` span. At the
canonical row-height of 17px, 12.5% resolves to 2.125px — a sub-pixel
boundary the browser rounds inconsistently from cell to cell. The result
is a visible per-cell jog along the rule and 1px gaps/overlaps against
the row immediately below (typically `│` box-drawing or empty cells).

Fix: route every percentage-based vertical block gradient stop through
`round(calc(var(--term-row-height) * N/8), 1px)` so the boundary lands
on a whole device pixel. All U+2580..U+2587 and U+2594 stops are
converted in one pass — they share the same sub-pixel hazard, U+2594 is
just the one Claude Code's UI surfaces most often.

Horizontal-axis gradients (U+2589..U+258F, U+2590, U+2595) and the
quadrant codepoints are unaffected because their gradient line is the
cell width (1ch ≈ font-advance), and the rounding hazard is far less
visible across cells than along a continuous horizontal rule.

Tests: `packages/@wterm/dom/src/__tests__/upper-block-alignment.test.ts`
asserts the emitted CSS string contains a pixel-typed stop (no bare
`12.5%`) and that every U+2594 cell paints with the same background
string so adjacent cells stay on the same pixel.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

@codevibesmatter is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@codevibesmatter
Copy link
Copy Markdown
Author

FYI cross-link: validated this stacked on top of #75 against Claude Code's TUI on our staging deploy — the combined branches fix both the U+2594 horizontal-rule jog (this PR) and the wide-char layout displacement (#75). Comment with details on #75. Both fixes are needed for clean Claude TUI rendering; this one is the simpler self-contained piece.

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