WebCraft

scroll-state(stuck)

A sticky header that knows it is stuck

Ledger

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.

HTML + CSS
<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

<details name>

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.