I was building a block theme (FSE) for a client's site, and the hero was designed to start flush at the top of the viewport: full-bleed, with the nav floating over it, no gap. But the moment I opened it in a browser, there was a thin white strip, roughly 24px tall, wedged between the site nav and the hero section. Not a huge band, but enough to ruin the "nav hovering over the image" effect I was after.
My first instinct was a bad one, as usual. I assumed the hero section itself carried a stray margin, so I opened DevTools and started stripping margins off the hero elements one at a time. Nothing moved. The white strip was stubborn. I checked padding on the header template-part, checked margin on <main>, checked the first section. Every time I clicked an element, DevTools highlighted the empty space but never pointed at whoever created it. It felt like a phantom margin.
Why this happens
The key clicked when I stopped inspecting individual elements and started looking at the box-model overlay around the header-and-main seam. That 24px of space was not padding, it was margin-top. And 24px was a familiar number: it is the default value of --wp--style--block-gap.
WordPress block layout injects a default gap via --wp--style--block-gap. That gap does not just live inside a single container. It propagates as margin-top between sibling elements at the block layout level, including between the header template-part and the following <main>, and again between <main> and its direct children. So my hero was not pushed down just once, it was pushed by the block-gap flowing down the whole chain: template-part to main, then main to the first section.
That is what made it feel like a ghost. There was not a single CSS rule in my stylesheet that wrote margin-top: 24px. The number was born inside the block theme.json system and injected via a custom property, so in DevTools it showed up as a "computed" margin with no source I could click through to in my own files. I was hunting the culprit in the wrong place because the culprit was not my code, it was a WordPress default.
The fix
The fix is not to change --wp--style--block-gap globally, because that gap is useful elsewhere, inside the content. The fix is to zero the block-default margins surgically, exactly on the chain that pushes the hero down, and leave the rest intact.
The chain I had to target was three seams: the template-part-plus-main sibling, main itself along with its direct children, and the first section. I set margin-top and margin-block-start to 0 on all of them, plus gap: 0 on main:
.wp-site-blocks > .wp-block-template-part + main,
main.page_wrap,
main.page_wrap > .page_main,
.page_wrap > * {
margin-top: 0 !important;
margin-block-start: 0 !important;
}
main.page_wrap {
gap: 0;
}
.page_main > section:first-child {
margin-top: 0 !important;
}Why margin-block-start too, not just margin-top? Because block layout sometimes writes the logical margin (margin-block-start), not the physical one. If I only zeroed margin-top, the logical rule could win and the strip would survive. Zeroing both closes off both paths at once.
Why !important? Because the block-gap value is injected with a high specificity through WordPress-generated selectors. Here !important is not laziness, it is genuinely the cleanest way to beat a rule whose source I do not control.
Once this rule landed, I rechecked. The hero now starts exactly at rect.y = 0, flush with the top of the viewport, and the nav floats over it precisely as designed. The phantom white strip was gone completely.
The takeaway
When there is a mysterious empty space in a block theme and no single element confesses to causing it, suspect --wp--style--block-gap first. WordPress block layout injects a default margin between siblings at the layout level, so that space tends to appear at structural seams, header-to-main, main-to-children, not inside the components you wrote yourself. Do not waste time stripping margins off your components; target the chain of structural seams, zero margin-top and margin-block-start together, and set gap: 0 on main if needed. Since then, when a nonsensical 24px white strip shows up, I no longer hunt for a stray margin in my CSS, I just ask: is this block-gap?
