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 aposition
value ofsticky
is stuck to an edge of its scroll container ancestor. This is useful for stylingposition: 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:
.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:
@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:
<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.
.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.
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.
@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.
<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.
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.
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.
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.
@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 thatnone
queries will match even if the container does not haveposition: 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:
<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.
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.
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>
.
@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.