Using container scroll-state queries

Container scroll-state queries are a type of container query. Rather than selectively applying styles to descendant elements based on the container's size, scroll-state queries allow you to selectively apply styles to descendent elements based on the container's scroll state. This can include whether the container is partially scrolled, snapped to a scroll snap container ancestor, or positioned via position: sticky and stuck to a boundary of a scroll container ancestor.

This article explains how to use container scroll-state queries, walking through an example of each type. It assumes that you know the basics of container queries. If you are new to container queries, read CSS container queries before continuing.

Types of container scroll-state query

There are three @container descriptors you can use in a scroll-state() query:

  • scrollable: Queries whether a container can be scrolled in the given direction via user-initiated scrolling (for example by dragging the scrollbar or using a trackpad gesture). In other words, is there any overflowing content in the given direction that can be scrolled to? This is useful for applying styling related to the scroll position of a scroll container. For example, you could display a hint that encourages people to scroll down and see more content when the scrollbar is up at the top, and hide it when the user has actually started scrolling.
  • snapped: Queries whether a container is, or will be, snapped to a scroll snap container ancestor along a given axis. This is useful for applying styles when an element is snapped to a scroll snap container. For example, you might want to highlight a snapped element in some way, or reveal some of its content that was previously hidden.
  • stuck: Queries whether a container with a position value of sticky is stuck to an edge of its scroll container ancestor. This is useful for styling position: sticky elements differently when stuck — for example, you could give them a different color scheme or layout.

Syntax overview

To establish a container element as a scroll-state query container, set the container-type property on it with a value of scroll-state. You can optionally also give it a container-name, so that you can target it with a specific container query:

css
.container {
  container-type: scroll-state;
  container-name: my-container;
}

You can then create a @container block that specifies the query, the rules that are applied to children of the container if the test passes, and optionally, the container-name of the container(s) you want to query. If you don't specify a container-name, the container query will be applied to all scroll-state query containers on the page.

Here, we query only containers named my-container to determine whether the container can be scrolled towards its top edge:

css
@container my-container scroll-state(scrollable: top) {
  /* CSS rules go here */
}

Note: To separate scroll-state queries from other container queries, the scroll-state descriptors and value are placed inside parentheses, preceded by scroll-state (scroll-state( ... )). These constructs look like functions, but they're not.

Using scrollable queries

Scroll-state scrollable queries, written as scroll-state(scrollable: value), test whether a container's scrolling ancestor can be scrolled in the given direction via user-initiated scrolling. If not, the query returns false.

The value indicates the direction you are testing for scrolling availability in, for example:

  • top: Tests whether the container can be scrolled towards its top edge.
  • inline-end: Tests whether the container can be scrolled towards its inline-end edge.
  • y: Tests whether the container can be scrolled in either or both directions along its y axis.

If the test passes, the rules inside the @container block are applied to descendants of the matching scroll container.

Let's look at an example in which we have a scrolling container full of content, and a handy little link to scroll back to the top if wished. We will use a scrollable query to only show the link when the user has started to scroll down through the content.

HTML

In the HTML we have an <article> element containing enough content to cause the document to scroll, preceded by a back-to-top link:

html
<a class="back-to-top" href="#" aria-label="Top of page">↑</a>
<article>
  <h1>Reader with container query-controlled "back-to-top" link</h1>
  <section>
    <header>
      <h2>This first section is interesting</h2>

      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </header>

    ...
  </section>

  ...
</article>

We have hidden most of the HTML for brevity.

CSS

The .back-to-top link is given a position value of fixed, placed at the bottom-right corner of the viewport, and moved off the viewport using a translate value of 80px 0. A transition value will animate the translate and background-color when either value changes.

css
.back-to-top {
  width: 64px;
  height: 64px;
  color: white;
  text-align: center;
  position: fixed;
  bottom: 10px;
  right: 10px;
  translate: 80px 0;
  transition:
    0.4s translate,
    0.2s background-color;
}

The scroll container in this example is the <html> element itself, denoted as a scroll-state query container with a container-type value of scroll-state. The container-name isn't strictly necessary but is useful in cases where the code is added to a codebase with multiple scroll-state query containers targeted with different queries.

css
html {
  container-type: scroll-state;
  container-name: scroller;
}

Next, we define a @container block that sets the container name targeted by this query, and the query itself — scrollable: top. This query applies the rules contained inside the block only if the <html> element can be scrolled toward its top edge — in other words, if the container has previously been scrolled downwards. If that is the case, translate: 0 0 is applied to the .back-to-top link, which transitions it back on-screen.

css
@container scroller scroll-state(scrollable: top) {
  .back-to-top {
    translate: 0 0;
  }
}

We've hidden the rest of the example CSS for brevity.

Result

Try scrolling the document down, and note how the "back-to-top" link appears as a result, animating smoothly from the right side of the viewport due to the transition. If you scroll back to the top by activating the link or manually scrolling, the "back-to-top" link transitions off-screen.

Using snapped queries

Relevant only when scroll snapping is implemented, scroll-state snapped queries (written as scroll-state(snapped: value)) test whether a container is, or will be, snapped to a scroll snap container ancestor along the given axis. If not, the query returns false.

The value in this case indicates the direction you are testing the element's ability to snap in, for example:

  • x: Tests whether the container is snapping horizontally to its scroll-snap container ancestor.
  • inline: Tests whether the container is snapping to its scroll-snap container ancestor in the inline direction.
  • y: Tests whether the container is snapping to its scroll-snap container ancestor in both directions.

To evaluate a container with a non-none snapped scroll-state query, it must be a container with a scroll-snap container ancestor, that is, the ancestor has a scroll-snap-type value other than none. The container query scroll-state(snapped: none) matches scroll-state containers that do not have a scroll container ancestor.

Evaluation will occur when the scrollsnapchanging event fires on the scroll snap container.

If the test passes, the rules inside the @container block are applied to descendants of the matching scroll snap target container.

In this example, we'll look at a scroll snap container with children that snap to it vertically and use a snapped query to style the children only when they are snapped or about to be snapped.

HTML

The HTML consists of a <main> element that will be a scroll snap container. Inside are several <section> elements that will be snap targets. Each <section> contains a wrapper <div> and an <h2> heading. The wrappers are included to create a style target as container queries enable styling a container's descendants, not the container itself.

html
<main>
  <section>
    <div class="wrapper">
      <h2>Section 1</h2>
    </div>
  </section>

  ...
</main>

We have hidden most of the HTML for brevity.

CSS

We set an overflow value of scroll and a fixed height on the <main> element to turn it into a vertical scroll container. We also set a scroll-snap-type value of y mandatory to turn <main> into a scroll snap container that snap targets will snap to along the y axis; mandatory means that a snap target will always be snapped to.

css
main {
  overflow: scroll;
  scroll-snap-type: y mandatory;
  height: 450px;
  width: 250px;
  border: 3px solid black;
}

The <section> elements are designated as snap targets by setting a non-none scroll-snap-align value. The center value means that they will snap to the container at their center points.

css
section {
  font-family: Arial, Helvetica, sans-serif;
  width: 150px;
  height: 150px;
  margin: 50px auto;

  scroll-snap-align: center;
}

We want to enable the <section> elements to be queried. Specifically, we want to test whether the <section> elements are snapping to their container, so we denote them as scroll-state query containers by setting a container-type value of scroll-state on them. We also give them a container-name, which isn't strictly necessary, but will be useful if our code gets more complex later and we have multiple scroll-state query containers that we want to target with different queries.

css
section {
  container-type: scroll-state;
  container-name: snap-container;
}

Next, we define a @container block that sets the container name we are targetting with this query, and the query itself — snapped: y. This query applies the rules contained inside the block only if a <section> element is being snapped vertically to its container. If that is the case, we apply a new background and color to the <section> element's child .wrapper <div> to highlight it.

css
@container snap-container scroll-state(snapped: y) {
  .wrapper {
    background: purple;
    color: white;
  }
}

Result

The rendered result is shown below. Try scrolling the container up and down, and note how the <section> style changes when it becomes snapped to its container.

Using stuck queries

Scroll-state stuck queries, written as scroll-state(stuck: value), test whether a container with a position value of sticky is stuck to an edge of its scroll container ancestor. If not, the query returns false.

The value in this case indicates the scroll container edge you are testing, for example:

  • top: Tests whether the container is stuck to the top edge of its scroll container ancestor.
  • block-end: Tests whether the container is stuck to the block-end edge of its scroll container ancestor.
  • none: Tests whether the container is not stuck to any edges of its scroll container ancestor. Note that none queries will match even if the container does not have position: sticky set on it.

If the query returns true, the rules inside the @container block are applied to descendants of the matching position: sticky container.

Let's look at an example where we have a scrolling container with overflowing content, in which the headings are set to position: sticky and stick to the top edge of the container when they scroll to that position. We will use a stuck scroll-state query to style the headings differently when they are stuck to the top edge.

HTML

In the HTML, we have an <article> element containing enough content to cause the document to scroll. It is structured using several <section> elements, each containing a <header> with nested content:

html
<article>
  <h1>Sticky reader with scroll-state container query</h1>
  <section>
    <header>
      <h2>This first section is interesting</h2>

      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
    </header>

    ...
  </section>

  <section>
    <header>
      <h2>This one, not so much</h2>

      <p>Confecta res esset.</p>
    </header>

    ...
  </section>

  ...
</article>

We have hidden most of the HTML for brevity.

CSS

Each <header> has a position value of sticky and a top value of 0, which sticks them to the top edge of the scroll container. To test whether the <header> elements are stuck to the container top edge, they are denoted as scroll-state query containers with a container-type value of scroll-state. The container-name isn't strictly necessary but will be useful if this code gets added to a code base with multiple scroll-state query containers targeted with different queries.

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

We also give the <h2> and <p> elements inside the <header> elements some basic styling, and a transition value so they will smoothly animate when their background values change.

css
h2,
header p {
  margin: 0;
  transition: 0.4s background;
}

h2 {
  padding: 20px 5px;
  margin-bottom: 10px;
}

header p {
  font-style: italic;
  padding: 10px 5px;
}

Next, we define a @container block that sets the container name we are targetting with this query, and the query itself — stuck: top. This query applies the rules contained inside the block only if a <header> element is stuck to the top of its scroll container. When that is the case, a different background and a box-shadow are applied to the contained <h2> and <p>.

css
@container sticky-heading scroll-state(stuck: top) {
  h2,
  p {
    background: #ccc;
    box-shadow: 0 5px 2px #0007;
  }
}

We have hidden the rest of the CSS for brevity.

Result

Try scrolling the document down and up, and note how the <h2> and <p> elements transition to a new color scheme when they become stuck to the top of their container's top edge.

See also