Scope widened after filing. This started as the strip below a tabbed screen. Measuring against an app that gets it right shows the same defect at the top as well, from a different cause. Both are covered here; see the comment with the Instagram comparison for the numbers and the split.
Acceptance criterion: no row of the display is the app's background colour where content exists to fill it. Instagram on iOS 26 measures 0 dead rows at both edges. A Craft app measures 39pt at the top and 34pt at the bottom.
v0.0.387 shipped a fix for #425. On a real app it changes nothing. The grey strip is still there, and it needs to be gone — not narrowed, not conditional, gone.
Measured, not eyeballed
iPhone 17 Pro simulator, iOS 26.5, 402x874pt. Same app, same screen, three builds. Lowest row containing anything other than flat background:
| build | content reaches | dead strip |
|---|---|---|
| craft-native 0.0.380 (before the fix) | y=2558 of 2622 | 21pt |
| craft-native 0.0.387 (with the fix) | y=2558 of 2622 | 21pt |
| 0.0.387, different screen, fresh build | y=2558 of 2622 | 21pt |
Identical. The fix did not move a single pixel on any screen we ship.
The strip samples rgb(241,241,246) at the screen edges, falling to rgb(236,236,241) under the bar's shadow. backgroundColor in our craft.config.json is #f2f2f7 = rgb(242,242,247). It is the app's background, painted where content should be.
What a user sees: the floating tab bar's own translucency shows live content behind the pill, and then a flat grey band under it, running the full width to the bottom of the screen. The content that should show through below the bar simply is not there. Nothing else on iOS does this. Phone, Reddit, Instagram and Apple's own Music do not have it.
Why the v0.0.387 fix never engages
CraftNativeScreen.swift:
var tabbedScroll = false
if let scroll, scroll === leadingScroll?.view, scroll.bounds.height > 0,
... {
let viewport = scroll.convert(scroll.bounds, to: rootStack)
tabbedScroll = abs(viewport.maxY - rootStack.bounds.maxY) < 0.5
}
let bottomEdgeToEdge = fullEdgeToEdge || tabbedScrollTwo clauses rule out ordinary screens:
scroll === leadingScroll?.view— the scroll view has to be the root's leading child. Ours is nested two levels down, under a positioning wrapper and a screen wrapper. That is not exotic; it is what any screen with a modal, an overlay or a web/native fork looks like.viewport.maxY == rootStack.bounds.maxY— the scroll view has to be the last thing in the root stack. Ours has siblings after it: a modal block and a notice block, both normally not rendering, both still children.
So tabbedScroll is false on every screen we have, rootBottomToSafeArea stays active, and the behaviour is exactly what it was before the fix. The fix is written for a screen whose root is a scroll view. Real screens are not shaped that way, and a fix that only applies to the simplest possible hierarchy does not fix the bug.
What this needs to be
No screen should ever paint the app's background colour into the strip between its content and the bottom of the display. Not 21pt of it, not 1pt. If a tab bar is on screen, content runs to the bottom of the display and the bar floats over it, with the scroll view's content inset — which Craft already sets to .automatic — providing the clearance. That is the whole of the behaviour, and it should not depend on where the scroll view sits in the hierarchy.
Concretely, one of:
- find the screen's primary vertical scroller wherever it is in the tree rather than requiring it to be the leading child, and ignore siblings that render nothing; or
- drop the structural test entirely: when a visible tab bar overlaps the screen, pin the root to
view.bottomAnchorand let content inset handle clearance. A screen with no scroll view keeps its safe-area bottom, which is the one case the current guard is genuinely protecting.
The careful guards in the current implementation — checking the bar is visible, in the same window and overlapping — are right and worth keeping. It is the structural requirement that defeats it.
Repro
Any screen shaped like this, in a tabbed app, on iOS 26:
<div class="relative flex-1">
<div class="flex flex-1 flex-col">
<div data-native="ScrollView" class="flex flex-1 flex-col"> ... </div>
<div :if="showModal"> ... </div>
</div>
</div>Screenshot it and sample the bottom 21pt. It is backgroundColor, not content.
Happy to test a build against a real app with ~90 screens the moment there is one.