Lessons Learned Rewriting a Sticky Detector
Lessons Learned Rewriting a Sticky Detector êŽë š
I recently came across a demo I did in 2014 (chriscoyier) where, as you scroll down, a search input becomes stuck to the top of the page, and changes styles a bit to make room for a hamburger menu. Iâll just video that so itâs clear:
Perhaps you forget these long ago times, but we were not rocking position: sticky; back then. So this demo had to replicate that somehow. But I was noticing how the styles change as well, meaning even after we got position: sticky;, weâd need a way to detect when itâs stuck (through JavaScript or whatever) and update a class (through JavaScript or whatever).
In these cushy, simpler times, we can get this all done in CSS. But there are a couple of other lessons to be learned by converting this thing to 2026 magic, so letâs go.
The Main Problem
In the old demo, the main problem is⊠JavaScript. Itâs not even that JavaScript is used at all, itâs just that how itâs implemented is wildly inefficient.
var wrap = $("#wrap");
wrap.on("scroll", function(e) {
if (this.scrollTop > 147) {
wrap.addClass("fix-search");
} else {
wrap.removeClass("fix-search");
}
});
This listens to the scroll event and fires a callback on each. One little scroll flick downwards might fire this event (and thus do the callback and the DOM element access and math) many hundreds of times. Boooooo.

Plus, itâs jQuery 2.1.3 just because hey, it was 2014. ---
Gettinâ Sticky
The first thing weâre gonna do is use position: sticky; as thatâs going to do the main thing we need (stick the search input to the header as it scrolls by), and we can toss the old JavaScript entirely. Weâre gonna need a wrapper element that will be the element gettinâ stuck. So we can do this:
<div class="search-area">
<form action="/search">
<label for="s" class="visually-hidden">Search</label>
<input id="s" type="search" placeholder="Search..." />
</form>
</div>
So even if this area isnât at the top of the page to start, and ours isnât, itâll get stuck to the top when we scroll by if we do this:
.search-area {
position: sticky;
top: 0;
}
Using <search>
Perhaps the <form> is a bit presumtive there. I might argue itâs a good idea since itâs the kind of thing that is the foundation of progressive enhancement and working without JavaScript. But this is literally a fake example that doesnât search anything. We could replace that form with a <search> element or use <search> as the parent element.
<div class="search-area">
<search>
<label for="s" class="visually-hidden">Search</label>
<input id="s" type="search" placeholder="Search..." />
</search>
</div>
We basically get a free ARIA landmark for using it.
Stuck State Query
I also chucked that <search> in there for a sneaky second reason. We need an additional child element to style, since weâre utlimately going to use a container query, and you canât write container styles on the container itself. Classic gotcha.
Letâs officially make the wrapper element a container:
.search-area {
/* `scroll-state` is a real keyword we need to make this specific kind of container */
container-type: scroll-state;
/* `sticky-search` is a name we just made up to name the container */
container-name: sticky-search;
position: sticky;
top: 0;
}
Now we can write a @container query to detect the stuck state and use it. We could it like this, without needing the name at all:
.search-area {
container-type: scroll-state;
container-name: sticky-search;
position: sticky;
top: 0;
@container scroll-state(stuck: top) {
search {
/* stuck styles for a child element */
}
}
}
Or do it elsewhere in the CSS and call it by name:
...
search {
...
@container sticky-search scroll-state(stuck: top) {
/* stuck styles for a child element */
input {
/* keep styling other stuff */
}
}
}
In our demo, we add a bit of padding, change the background, and fiddle with the width of the input when itâs stuck (as thatâs what the original demo did as well).
Anchor the Burger
This is another somewhat contrived situation because of the nature of the demo, but itâs useful for learning anyway. See how weâve kind of make a âfakeâ mobile layout by limiting the wrapper area to 280px? Itâs in that area specifically where we want our position: fixed; hamburger menu. But there is another classic gotcha here where you canât position: fixed; and element relative to some random element on the page. You canât just position: relative; a parent like you do with position: absolute;. There is trickery, like applying an unnecessary transform to scope things, but I find the tradeoffs unacceptable. Plus, there is a better way, anyway.
We can express âput this thing relative to this other element and stay thereâ through anchor positioning these days, without any of the weird constraints.
Since we have an obvious parent element at play here (it might just be the <body> on a typical situation), weâll make it the anchoring context like this:
.wrap {
/* ... */
anchor-name: --wrap;
}
Now we can still use position: fixed;, but place it perfectly relative to that anchor we just set up.
.menu-icon {
/* ... */
position: fixed;
position-anchor: --wrap;
top: calc(anchor(top) + 12px);
right: calc(anchor(right) + 25px);
}

I love this so much.
Use SVG
The old demo used â° for the burger icon, which Iâm just gonna say is a bad practice. Using <svg> is just better, and allows for some normal markup. Probably something like:
<button class="menu-icon" aria-label="Toggle Menu">
<svg viewBox="0 0 100 100" width="12" height="12" aria-hidden="true">
<path d="..." />
<!-- or maybe some <line> stuff -->
</svg>
</button>
I just grabbed one out of CodePenâs free assets:
