Skip to content

My Sticky Sidebar Failed in Complete Silence

Adityo Guni Waluyo

A position sticky sidebar that scrolled away with the page, and the three CSS preconditions I now check before trusting sticky again.

TL;DR

A sticky sidebar in a flex layout failed silently because it broke all three preconditions. Flexbox's default stretch left the aside no scroll room, fixed with items-start, while a fixed header demanded a custom top offset. The author now avoids overflow-clipping ancestors, removed redundant height caps, and made this layout a standard pattern.

That afternoon I opened the fleet dashboard, scrolled a little to check the server at the bottom, and got suspicious right away. The alert-history sidebar I had just mounted on the right edge was supposed to stick around while the page scrolled. Instead, it rode up and out together with the content. No console error, no warning, nothing at all. Just a position: sticky that refused to work, on a screen that looked perfectly normal.

The layout back then was simple: one flex row holding the main column on the left and an aside on the right. I wrote the first version with Tailwind classes on the aside roughly like this:

<aside class="w-full lg:w-[30%] lg:min-w-[280px] lg:max-w-[380px]
  shrink-0 lg:sticky lg:top-0 lg:max-h-screen lg:overflow-y-auto">
  ... alerts ...
</aside>

As far as I was concerned it was complete: sticky, an offset, a height cap, internal scroll. The next three commits went into guessing: nudge the offset, swap the height, add a border. None of it reached the root cause, because the problem was never the numbers. Sticky has three preconditions, and I skipped every one of them; each one fails in silence [1].

Flexbox stretch eats the scroll room

First precondition: a sticky element only sticks within its parent's box. If the parent is already as tall as the viewport, no scroll distance remains inside it, so sticky never gets a moment to activate. This is where flexbox swings the hammer. The initial value of align-items on a flex container is stretch, so every flex item gets pulled to the height of the tallest one [2]. My main column runs hundreds of pixels down the page, and the aside stretched right along with it. The aside's parent became as tall as the page, flush with the viewport at the start, and the sticky behavior slept through all of it.

The fix is one word: items-start on the flex row. The aside's height goes back to following its own content, and now there is scroll room for it to travel through. What annoyed me the most: one barely-considered default property quietly turned my five sticky classes into decoration.

The inset must be non-auto, and a fixed header complicates it

Second precondition, the offset. The MDN docs say it outright: if the inset properties on one axis are all auto, sticky behaves like relative on that axis [1]. I already had lg:top-0 in place, so this one was a formality? Not quite, because my dashboard header is fixed. top-0 parks the sidebar right under the viewport's mouth, exactly behind the header. This was the least elegant part of the whole saga: the first commit set the offset to 48 pixels, the second to 50, the third settled on 55 plus a 1px border to separate it cleanly from the header. Three commits to hunt one number, because the value depends on my own header's height; there is no universal constant to copy from anywhere.

The overflow ancestor that swallows sticky

Third precondition, the sneakiest one: a sticky element sticks to its nearest ancestor with a scrolling mechanism, and an ancestor with overflow hidden, scroll, or auto counts, even when it never actually scrolls [1]. One overflow-hidden utility added to round a card's corners, and sticky gets collected inside that wrapper, never reaching the viewport. I now make a habit of walking the ancestor chain from the aside upward, making sure nothing clips overflow before the page root.

Both mechanics above are now wrapped up neatly in the official documentation. Tailwind's own page describes sticky exactly the way I experienced it: relative until it crosses a threshold, then fixed until its parent scrolls off screen [3]. The design is good. What raises my blood pressure is how it fails: silently, without a single line of error.

The 400px cap that turned into junk

One leftover from the early chaos. In the first version I carried the alert list inside a max-h-[400px] container with internal scroll, afraid the list would run too long. Once the stretch fix landed and lg:max-h-screen actually worked on the aside, that 400px cap became redundant. The sidebar's height was already capped by the viewport, and the internal scroll moved to the right level. I removed it. Code that grows out of guesses loves to leave unused gear behind.

Sticky is now on my list of properties I refuse to apply casually. I check its three preconditions in order: a non-auto inset, scroll room in the parent, and no overflow-clipping ancestor on the way up to the viewport. If one of them is missing, I know exactly which one to open first. Going forward, this two-column-plus-sticky layout becomes the default pattern for every panel page I build, with the offset tied to the header height instead of a guessed sticker number.

Sources

  1. MDN Web Docs: position
  2. MDN Web Docs: Aligning items in a flex container
  3. Tailwind CSS: position

Related articles