Skip to content
← writing

A sticky navbar that morphs when it sticks, no JS

css
container-queries
scroll
frontend

You know the move: a full-width navbar that, once you scroll past it, shrinks into a floating pill. Until recently that meant a scroll listener, an IntersectionObserver, or a sprinkle of JS toggling a class. Not anymore. Manu Arora showed it off with pure CSS, and the key is a feature I'd missed: scroll-state container queries.

seangeng
workwritingabout

Scroll this box. The bar starts edge-to-edge, then shrinks into a floating pill the moment it sticks to the top.

Scroll the box above (in a Chromium browser). The bar starts edge-to-edge, then the instant it sticks, it pulls in and rounds off into a pill.

The whole thing

header {
  container-type: scroll-state;
  position: sticky;
  top: 0;
}
 
@container scroll-state(stuck: top) {
  .nav-bar {
    max-width: 56rem;
    border-radius: 0.75rem;
    background: rgb(255 255 255 / 0.92);
  }
}

Two parts. container-type: scroll-state turns the sticky header into a container the browser tracks scroll state for. Then @container scroll-state(stuck: top) matches whenever that container is currently stuck to the top, and you restyle anything inside it. The browser does the detecting; you just write the stuck-state styles. Add a transition on the inner element and the morph animates for free.

The catch

It's Chromium-only right now. No Firefox, no Safari, which is a shame for something this clean. But it degrades nicely: where scroll-state isn't supported, the query just never matches and your navbar stays in its default state. So treat the morph as an enhancement, make sure the un-morphed bar is perfectly usable on its own, and you can ship it today without breaking anyone.

Via @mannupaaji.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/sticky-navbar-that-morphs-on-scroll.md and implement it in my project.

It covers: A sticky navbar that morphs when it sticks, no JS — scroll-state container queries let the browser tell you when a sticky element is stuck, so you can restyle it on scroll with pure CSS. No scroll listeners, no animation library. Chromium-only for now.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md