Skip to content

Creating Full-Bleed Card Rows Without Horizontal Scrollbars

Adityo Guni Waluyo

Split a card list into two rows; row 2 breaks out of the container with w-screen and a calc margin, with no horizontal scrollbar.

TL;DR

w-screen breaks layouts because 100vw ignores the scrollbar, leaving your element a few pixels too wide. Fix it with the full-bleed combo w-screen, max-w-[100vw], ml-[calc(50%-50vw)], and padding, then scope overflow-hidden to just that section. Use overflow-clip instead if a sticky header breaks, and an IIFE renderCard to split rows without duplicating markup.

I was reviewing the homepage layout in about-teaser.tsx. The design had a specific request: the first row of company cards needed to stay centered with the standard container, but the second row had to stretch full-width from edge to edge. I immediately typed w-screen on the second row, thinking the problem was solved.

When I refreshed the browser, a horizontal scrollbar appeared at the bottom. The layout shifted slightly to the right. Yet the container above it was clean and nothing seemed off.

At first I thought it was just the browser's default margin not being reset. I tried zeroing out the margin, but the scrollbar stayed. I even suspected another element was leaking outside the container.

Turns out, w-screen or 100vw is tricky. The specification clearly states that the vw unit is calculated based on 1% of the initial containing block width, and assumes no scrollbar exists [1]. On desktop browsers with classic scrollbars, an element at 100vw ends up wider than the visible document area, triggering horizontal overflow [3]. That's why the full bleed grid technique often fails if you just go full-width alone.

The Full-Width Trap

The problem isn't the intention, it's the CSS execution. Assuming w-screen alone is enough breaks the layout. The element is indeed viewport-width, but the viewport here is calculated before the scrollbar is accounted for. The result is our element being a few pixels too wide (about 12px on my machine, though the number can vary depending on OS and browser) [3].

If we leave it, the browser treats this as valid content and shows a horizontal scrollbar. This seriously messes with user experience, especially for just one row of cards.

The Right Margin and Clipping Formula

The solution is shifting the element left by exactly the scrollbar overflow amount, then clipping the overflow.

First, I used the class combination w-screen max-w-[100vw] ml-[calc(50%-50vw)] px-md. The left margin calc(50% - 50vw) is what shifts the element from the container center toward the actual left edge of the screen. This pattern is known as full-bleed, a term borrowed from print design: content printed all the way to the paper edge [4]. And this is disciplined negative margin in action. Small note: px-md on the second row keeps cards from sticking to the physical screen edge; the width is full, but the content still breathes.

Second, and this is the part that matters most, I had to add overflow-hidden to the wrapping <section> element. Never put this on body or html. If you put it at the topmost level, you'll end up clipping other content that actually needs vertical scrolling. By scoping it only to that section, we safely clip the excess pixels without breaking the global layout.

There's another trap that rarely gets discussed. overflow-hidden makes an element a scroll container, and scroll containers can be hijacked [10]. A position: sticky element always sticks to the nearest ancestor with a scrolling mechanism, and overflow: hidden creates that mechanism, even if that ancestor is never scrolled [11]. Put overflow-hidden on body or a tall wrapper, and a sticky header somewhere else suddenly stops working with no error at all. If you only need clipping without that side effect, overflow: clip clips without making the element a scroll container, and without creating a new formatting context [10].

These relative length units stay consistent in scale because child elements inherit computed values from their parents, so calculations remain accurate across screen sizes [2]. The bg-card class on this section isn't just cosmetic either. When using clipping, the section background must match the card theme so transitions are smooth and don't show clipped content behind it.

Keeping Code Clean with IIFE

In frontend/src/components/home/about-teaser.tsx, I have one data array called groupCompanies. The next challenge was splitting this data into two rows without writing the card markup twice.

I used a renderCard function as an IIFE (Immediately Invoked Function Expression). This lets me define how to render one card in one place, then call it repeatedly with different data. By defining it within the component scope, we avoid polluting the global namespace.

The first row uses groupCompanies.slice(0, 3) with grid sm:grid-cols-2 lg:grid-cols-3 inside the standard max-w container. This keeps the first three cards neatly centered.

The second row uses groupCompanies.slice(3) with grid lg:grid-cols-4. The second row's wrapper section gets the overflow-hidden bg-card classes, and its grid gets the breakout classes w-screen max-w-[100vw] ml-[calc(50%-50vw)] px-md.

With this approach, the full bleed grid technique stays clean without sacrificing scroll accessibility or making the code messy. I don't need to create a separate component just for one differently-laid-out row. If there's a design change later on one card, like adding a shadow or changing padding, we only need to update it in one place.

This pattern works anytime you need an element to break out of the main container but still want to keep the page free from accidental horizontal scrollbars. The key is always in the calculated margin and properly-scoped clipping combination.

Sources