Representative interview topic

Frontend Interview: When Can CSS scroll-state Queries Replace JavaScript Scroll Detection?

FrontendHard
Offer.cc Editorial TeamPublished Updated

Question

How would you use CSS scroll-state queries to respond to sticky, snap, or scrollable states instead of listening to scroll events in JavaScript?

Prompt and when it applies

Interview prompt: a page has a sticky heading, horizontal snap cards, and a hint that more content can be scrolled. How would you decide when to change styles? Compare CSS scroll-state queries with JavaScript scroll listeners, and explain what you would do when the browser lacks support.

The boundary is declarative styling: add a shadow when a heading sticks, highlight a snapped card, or show a hint while more content is scrollable. Business state, complex gestures, precise scroll progress, and server reporting still belong in JavaScript. MDN describes four scroll-state query families: scrollable, scrolled, snapped, and stuck.

What the interviewer is assessing

The interviewer wants to see whether you select the API by state type and then handle support and accessibility constraints. A strong answer says that the query container needs container-type and that the responding styles target descendants; it also avoids treating a CSS state query as a general event system.

A weak answer memorizes @container syntax. A strong answer explains when JavaScript remains necessary, how to use @supports for progressive enhancement, how to respect prefers-reduced-motion, and how to test real sticky, snap, overflow, and unsupported-browser cases.

Clarifying questions before you answer

First ask whether the requirement is visual feedback or business logic. Shadows, colors, and hint visibility fit CSS; recording scroll direction, making a network request, or driving a state machine needs JavaScript.

Next confirm the state source: is the element actually position: sticky, does the list set scroll-snap-type, and does the scroll container have visible overflow? Without those prerequisites, the relevant query remains false.

Finally confirm the browser baseline and motion requirements. Chrome for Developers documents scroll-state container queries in Chrome 133; a production design should start with styles that do not depend on the feature, wrap enhancements in supports, and honor reduced motion.

30-second answer framework

Say something like:

“I would separate style state from business events. Sticky, snap, and scrollability are browser states, so I would set container-type: scroll-state on the relevant element and use @container with scroll-state to style its descendants. For analytics, exact progress, or cross-component business actions, I would keep JavaScript. I would make the default CSS usable, add the feature behind @supports, disable nonessential motion for reduced-motion users, and test overflow, snap, keyboard input, and unsupported browsers.”

Step-by-step deep answer

Establish a scroll-state query container

Declare the element whose state will be queried as a scroll-state container. Give it a name when multiple containers could match:

css
.sticky-heading {
  position: sticky;
  top: 0;
  container-type: scroll-state;
  container-name: heading;
}

The responding rule must target a descendant, not the same element that owns container-type. Chrome’s guidance shows this proxy-layer relationship explicitly.

Choose the descriptor by state

stuck asks whether a sticky element is attached to an edge; snapped asks whether an element is about to snap to a snap container; scrollable asks whether overflow remains in a direction; scrolled asks about the most recent scroll direction. They answer different questions and are not interchangeable.

For example, add a shadow to navigation while it is stuck:

css
@container heading scroll-state(stuck: top) {
  .nav {
    box-shadow: 0 5px 12px rgb(0 0 0 / 18%);
  }
}

Put the style target below the container

Changing the background directly on the element that owns container-type makes the separation rule easy to violate. Keep an internal wrapper as the response target. The component stays reusable and the state transition is easier to test.

Use scrollable for discoverability

For a horizontal card list, show a gradient hint while the list can still scroll toward its inline end:

css
.carousel {
  container-type: scroll-state;
  container-name: cards;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

@container cards scroll-state(scrollable: inline-end) {
  .next-hint {
    opacity: 1;
  }
}

The hint is supplementary. Do not communicate “more content” only through color; retain keyboard scrolling, visible focus, and a text label.

Use snapped for current-card feedback

snapped is meaningful only when a snap ancestor and a valid snap axis exist. Highlighting the current card can improve visual feedback, but it is not a reliable business-selection commit signal. A real selection still needs an accessible control and JavaScript state.

Use scrolled for directional feedback

A top toolbar can react to the latest scroll direction, but directional movement can add cognitive load. Keep it accessible by default; use scrolled: block-start or block-end only when the benefit is clear, and add a transition.

Progressive enhancement and fallback

The base style should remain usable when scroll-state is unsupported:

css
.nav {
  box-shadow: none;
}

@supports (container-type: scroll-state) {
  .sticky-heading {
    container-type: scroll-state;
  }

  @container scroll-state(stuck: top) {
    .nav {
      box-shadow: 0 5px 12px rgb(0 0 0 / 18%);
    }
  }
}

If an old browser must receive a similar cue, use a narrow IntersectionObserver or ResizeObserver fallback. Do not recreate a full scroll-event loop in the fallback.

Motion and verification

Chrome recommends placing nonessential motion behind prefers-reduced-motion: no-preference. Test top and bottom edges, no overflow, keyboard operation, touch scrolling, snap intermediate states, zoom, and an unsupported browser. Confirm that state changes never hide focus or content.

High-quality sample answer

“I would first ask whether this is visual feedback or a business event. For a sticky shadow, snap highlight, or scroll hint, I would set container-type: scroll-state on the sticky or snapping element and use stuck, snapped, or scrollable queries so a descendant responds. Analytics, progress calculation, and selection submission stay in JavaScript. I would ship usable default CSS, add the enhancement behind @supports, keep a static fallback, and reduce motion when requested. I would test real overflow, snap, keyboard input, and older browsers.”

Common mistakes

Treating scroll-state as a scroll event

Failure pattern: using a query to fire analytics, a request, or a business submission. Why it fails: CSS expresses a style condition, not an event payload or reliable business timing. Fix: keep styling in CSS and business actions in semantic controls plus JavaScript.

Forgetting the query container

Failure pattern: writing @container scroll-state(stuck: top) without container-type: scroll-state. Why it fails: the browser has no state source to evaluate. Fix: verify the sticky, snap, or overflow prerequisite, then declare a named container.

Styling the container itself

Failure pattern: making one element both the container and the state-style target. Why it fails: the query targets descendants, and the structure becomes fragile. Fix: add an internal wrapper and apply the state feedback there.

Replacing accessible feedback with color

Failure pattern: changing only color to show the current snap card or more content. Why it fails: color alone does not convey state reliably and can exclude low-vision users. Fix: provide text, focus, controls, or an actionable scroll hint.

Ignoring support and motion boundaries

Failure pattern: assuming every browser supports the feature and forcing movement during scroll. Why it fails: older browsers degrade and motion can cause discomfort. Fix: use @supports, keep the default usable, and honor reduced-motion.

Follow-ups and responses

What if you need an exact scroll percentage?

scroll-state exposes discrete states, not a continuous percentage. Use JavaScript to read the scroll range and throttle updates, separating reads, computation, and writes so each scroll event does not synchronously trigger layout.

What if the sticky query is always false?

Check the sticky element’s containing block, top or inset, ancestor overflow, and whether the element actually enters its sticky range. Only then inspect container-type and the descendant selector.

What if the snap highlight flickers?

Check the snap axis, scroll-snap-align, and target dimensions. Avoid transitions on properties that frequently change layout. Keep focus and selection state explicit; highlighting is only visual state.

What if older browsers must look the same?

Define a usable static style first, then add a small IntersectionObserver or ResizeObserver fallback for the key cue. Do not copy a complex scroll-listener system; if enhancement costs more than it helps, accept a static shadow or always-visible hint.

What if the user prefers reduced motion?

Keep the information hierarchy without forced translation or scaling. Change only a shadow, border, or static color, and let prefers-reduced-motion override transitions and animations.

Public sources

Related questions