Lessons Learned Rewriting a Sticky Detector – Master.dev Blog
/ BLOG
CSSMenuScroll-StateSearchSticky
Lessons Learned Rewriting a Sticky Detector
Chris Coyier<br>on<br>July 15, 2026
I recently came across a demo I did in 2014 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) {<br>if (this.scrollTop > 147) {<br>wrap.addClass("fix-search");<br>} else {<br>wrap.removeClass("fix-search");<br>});Code language: JavaScript (javascript)
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"><br>form action="/search"><br>label for="s" class="visually-hidden">Searchlabel><br>input id="s" type="search" placeholder="Search..." /><br>form><br>div>Code language: HTML, XML (xml)
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 {<br>position: sticky;<br>top: 0;<br>}Code language: CSS (css)
Using
Perhaps the 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 element or use as the parent element.
div class="search-area"><br>search><br>label for="s" class="visually-hidden">Searchlabel><br>input id="s" type="search" placeholder="Search..." /><br>search><br>div>Code language: HTML, XML (xml)
We basically get a free ARIA landmark for using it.
Stuck State Query
I also chucked that 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 {<br>/* `scroll-state` is a real keyword we need to make this specific kind of container */<br>container-type: scroll-state;<br>/* `sticky-search` is a name we just made up to name the container */<br>container-name: sticky-search;<br>position: sticky;<br>top: 0;<br>}Code language: CSS (css)
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 {<br>container-type: scroll-state;<br>container-name: sticky-search;<br>position: sticky;<br>top: 0;
@container scroll-state(stuck: top) {<br>search {<br>/* stuck styles for a child element */<br>}Code language: CSS (css)
Or do it elsewhere in the CSS and call it by name:
...
search {<br>...
@container sticky-search scroll-state(stuck: top) {<br>/* stuck styles for a child element */
input {<br>/* keep styling other stuff */<br>}Code language: CSS (css)
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...