Skip to content

Stop iOS paragraphs drawing as one clipped line - #142

Merged
kevinmcalear merged 2 commits into
mainfrom
fix/last-section-text-clip
Sep 30, 2026
Merged

kevinmcalear merged 2 commits into
mainfrom
fix/last-section-text-clip

Conversation

@kevinmcalear

Copy link
Copy Markdown
Owner

The bug

On iOS, the drink page's last section (on main, Version of) sometimes drew its paragraph as one line running off the right edge ("...rank it with every otl"), with a blank line of reserved height under it. Web was fine.

Root cause

It isn't the body's gap, the marginTop: -space.xxl wrapper, or measurement caching. It's a React Native / Yoga rounding bug that only bites a paragraph at certain positions:

  1. On iOS, Fabric draws text in a TextKit box exactly the size of the view's frame (RCTTextLayoutManager drawAttributedString:... frame:).
  2. Yoga rounds each frame edge to the pixel grid in double, stores each as float32, then subtracts (roundLayoutResultsToPixelGrid). When the top is on a fractional offset (a 1/3 pt grid on 3x screens) and the paragraph crosses a power of two in absolute y (1024, 2048...), the two edges carry different float errors. The frame then comes out 47.99988 pt for 48 pt of text.
  3. TextKit fits one line fewer, and because RN uses NSLineBreakByClipping for text with no line limit, the whole paragraph lands on one clipped line.

On the drink page, the 1/3 pt hairlines in LockedSection (Service) and SpecRow put everything below them on fractional offsets. On a 393 pt phone the Service tiles (40% basis, aspectRatio: 1) do it too. So whichever section sat across y = 2048 broke, which is why it followed the last section around rather than its content. An empty <View /> after it can't help, because it doesn't move the paragraph.

Upstream: react/yoga#2011 (open), react/react-native#57920. React Native's iOS core ships prebuilt, so patching Yoga in node_modules wouldn't compile in.

The fix

The ds Text gets paddingBottom: textSlack (0.01 pt) on iOS only. That makes every text height fractional, and Yoga always rounds a fractional text height up ("never round down text"). So each paragraph gets up to 1/3 pt of slack wherever it lands: invisible, and far more than the float error. It's in the shared component, so every redesigned screen is covered, including PublicShell (same hero + body layout). No redesigned screen draws text outside ds Text. Web and Android are untouched. It's marked ponytail: to drop once RN ships the Yoga fix. JS only, so it can go out over the air.

components/ds/textSlack.check.ts models Yoga's rounding. It reproduces the device's numbers exactly (47.99993896484375 and 47.9998779296875) and checks every text style, 1 to 4 lines, on every grid offset down a 5000 pt page on 2x and 3x screens. It fails if the slack is set to 0.

Verified

iOS simulator (my own iPhone 17 Pro, iOS 26.5, fresh dev client from this branch, local Supabase, House Martini at Little Rye as Admin, full reloads only):

  • Before: a temporary harness put 720 copies of the Version of paragraph at every 1/3 pt step across y = 1024 and 2048. 96 of them got a 47.99994 or 47.99988 pt frame. In the screenshot, a copy straddling 2048 drew "Cocktail recommendation sytem #1 Is this a Martini? Link it to rank it with every" as a single line cut off at the right edge, with an empty second line. The harness also pushed the real Version of paragraph across 2048, and it showed the reported "...with every otl" clipping over a one-line gap above Link to Martini.
  • After: same harness, same positions. All 720 frames are 48.333 pt (smallest 48.33325), and the screenshot shows both the copy and the real Version of paragraph wrapping onto two lines ("...with every / other bar's Martinis."). The harness is not in this PR.

Web at 390 px: Version of wraps to two lines (358 x 48), and ds text has padding-bottom: 0px. At 1280 px (desktop layout with sidebar): the paragraph fits on one line (539 x 24) with no clipping.

Checks: npm run typecheck (with .expo/types moved aside, as in CI), npm run lint (0 errors, no new warnings), npm run check:design, npm run test:unit (45/45 checks, including the new one) and Jest (12 suites, 81 tests) all pass. Not run locally: build:web and test:security (no DB or RLS change); CI runs both.

🤖 Generated with Claude Code

On iOS, React Native draws text in a TextKit box exactly the size of the
view's frame. Yoga rounds each frame edge to the pixel grid and stores it as
float32, so a paragraph on a fractional offset that crosses y = 1024, 2048...
comes out about 0.0001 pt shorter than the height it measured. TextKit then
fits one line fewer, and with NSLineBreakByClipping the paragraph lands on one
line clipped at the edge, while the layout still reserves the full height.

On the drink page the 1/3 pt hairlines in LockedSection and SpecRow (and on
some phones the Service tiles) put everything below them on fractional
offsets, so whichever section sat across y = 2048 broke. Position, not
content, which is why it followed the last section around.

The ds Text now gets a hundredth of a point of bottom padding on iOS. That
makes its height fractional, and Yoga always rounds a fractional text height
up, so every paragraph gets up to 1/3 pt of slack wherever it lands. Web and
Android are unchanged. Upstream fix: react/yoga#2011 (open).

components/ds/textSlack.check.ts models Yoga's rounding and checks every
text style on every grid offset down a 5000 pt page on 2x and 3x screens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
babyvomit Ready Ready Preview Sep 30, 2026 11:10am UTC

Request Review

@github-actions

Copy link
Copy Markdown

Change routing: app

App (default). Green CI (typecheck, lint, unit tests, web build). Auto-eligible.

3 file(s) changed · rules: change-policy.yml on the base branch

@kevinmcalear
kevinmcalear merged commit f9c3f90 into main Sep 30, 2026
8 checks passed
@kevinmcalear
kevinmcalear deleted the fix/last-section-text-clip branch September 30, 2026 12:04

This branch was successfully deployed

1 active deployment
Preview — 2643aed7 Deployed Sep 30, 2026 by vercel[bot]
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