scroll-state(stuck)
A sticky header that knows it is stuck
0001 opening balance
0002 invoice raised
0003 payment received
0004 fee deducted
0005 invoice raised
0006 payment received
0007 closing balance
Caveatcontainer-type: scroll-state turns the sticky element into a query container, and nothing can match its own container query, so the state has to be read by something inside it. The header also needs a real inset such as top: 0 to stick against; with no inset it never reports as stuck and the rule never fires.
FallbackWhere scroll-state queries are unsupported the block inside simply never matches, so the header keeps the look it has before it sticks. Draw the resting state so it reads on its own and treat the stuck styling as the extra, not as the thing that makes the header legible.
<div class="head"><span>Ledger</span></div>
.head {
position: sticky; top: 0;
container-type: scroll-state;
}
/* the child reads the container's state */
@container scroll-state(stuck: top) {
.head span { color: var(--content); }
}
Published
Questions
How do I detect when a sticky element is stuck in CSS?
Make the sticky element a container with container-type: scroll-state and query it from a child with @container scroll-state(stuck: top). An element cannot match its own container query, so the rule has to land on something inside it.
Why is my scroll-state container query not matching?
Three things have to line up. The element is position: sticky, it has an inset such as top: 0 to stick against, and the rule targets a descendant rather than the container itself. A sticky element with no inset never reports as stuck.
Do I still need IntersectionObserver for a stuck header?
Not for styling. The old trick was a sentinel element one pixel above the header watched by IntersectionObserver, and a scroll-state query does the same job in two declarations with no script. Keep the observer only if something outside CSS has to know.