Get affordable and hassle-free WordPress hosting plans with Cloudways — start your free trial today.
Experimental: Check browser support before using this in production.
The ::scroll-marker
pseudo-element adds accessible scroll markers to scroll containers. The child elements of the scroll container will become scroll targets that users can scroll to (in a scroll-snapping manner) when the associated scroll marker is clicked on. ::scroll-marker
ultimately provides users with an additional way to navigate overflow content; however, successful implementation requires more than ::scroll-marker
by itself.
The following code declares styles for all scroll markers, declares their content (required) as blank, and declares the scroll-marker-group
property on the scroll container (required). Since the scroll container has five children, five scroll markers will be created inside a ::scroll-marker-group
pseudo-element placed ::after
the scroll container, thanks to the scroll-marker-group: after
declaration.
<ul class="scroll-container">
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
</ul>
.scroll-container {
scroll-marker-group: after; /* Required */
}
::scroll-marker {
content: ""; /* Required */
/* Scroll marker styles */
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid black;
}
Your mental model of the HTML code should be this if we were inspecting it in DevTools:
<ul class="scroll-container">
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<::scroll-marker-group>
<::scroll-marker>
<::scroll-marker>
<::scroll-marker>
<::scroll-marker>
<::scroll-marker>
</::scroll-marker-group>
</ul>
A scroll container can have as many scroll markers as necessary, which can be useful for carousels, scroll snapping components, and anywhere that you’d have scroll overflow content.
Optionally, we can combine scroll markers with ::scroll-button()
s and other scroll-based pseudo-elements and features.
And to top it all off, scroll makers are fully accessible too. They contain the correct semantics and accessible roles needed to be properly announced in screen readers, not to mention they support keyboard navigation right out of the box.
Syntax
::scroll-marker {
content: <scroll-marker-content>;
}
<scroll-marker-content>
: The scroll marker’s rendered content. Must be a valid value of thecontent
property (otherwise the marker won’t show).
There are two other requirements unrelated to the syntax that, if not met, will prevent markers from showing. First, the scroll targets must exist within a scroll container. The second requirement is that the scroll container must have the scroll-marker-group
property set to before
or after
. This is because the default value is none
, which causes the ::scroll-marker-group
pseudo-element that contains the markers to remain hidden.
Basic usage
Given this HTML:
<ul class="scroll-container">
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
<li class="scroll-target"></li>
</ul>
You’d target scroll markers universally, like this:
::scroll-marker {
/* ... */
}
Or, apply the properties to specific scroll targets, like this:
.scroll-target::scroll-marker {
/* ... */
}
And you must remember to include the content
property:
::scroll-marker {
content: ""; /* Required (even if blank) */
}
/* Select first markers */
:first-of-type::scroll-marker {
/* Plain text */
content: "First";
}
::scroll-marker {
/* From data attribute */
content: attr(data-tab-label);
}
/* Increment a counter */
li {
counter-increment: number;
}
li::scroll-marker {
content: counter(number);
}
Accessibility
Scroll markers are fully accessible right out of the box. For example, if you were focused on the first scroll marker, screen readers would announce “selected, tab, 1 of 5.” You can prefix a custom label onto this, though. To do that, you need to provide alternative text via the content
property:
/* No alt text */
::scroll-marker {
content: "Visual text"; /* Announces "selected, tab, 1 of 5" */
}
/* Alt text */
::scroll-marker {
content: "Visual text" / "Alt text"; /* Announces "Alt text, selected, tab, 1 of 5" */
}
Styling
Scroll markers are basically links, and this is something to consider when using text markers. For example, you might want to overwrite the color
and text-decoration
:
::scroll-marker {
content: "Tab 1";
color: inherit;
text-decoration: none;
/* etc. */
}
If you’d rather display stylized scroll markers, set the content
property to an empty string followed by the styles:
::scroll-marker {
content: "";
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid black;
}
And, of course, we’ll want to make it clear which scroll marker is currently selected. For that, you’ll need to use the :target-current
pseudo-class:
::scroll-marker:target-current {
background: black;
}
Finally, you’ll most likely want to put scroll-behavior: smooth
on the scroll container to enable smooth scrolling.
@media (prefers-reduced-motion) {
.scroll-container {
scroll-behavior: smooth;
}
}
Example: Carousel
<ul class="carousel">
<li style="background:hsl(10 70% 50%)"></li>
<li style="background:hsl(30 70% 50%)"></li>
<li style="background:hsl(50 70% 50%)"></li>
<li style="background:hsl(70 70% 50%)"></li>
<li style="background:hsl(90 70% 50%)"></li>
</ul>
.carousel {
/* The width */
--carousel-width: 100vw;
width: var(--carousel-width);
/* The height is half the width */
aspect-ratio: 1 / 0.5;
/* Implies flex-direction: row */
display: flex;
li {
/* Give carousel items the same width */
width: var(--carousel-width);
/* Prevent flexbox from overwriting said width */
flex-shrink: 0;
}
/* Show only one carousel item */
overflow: hidden;
/* Turn the carousel into an anchor */
anchor-name: --carousel;
/* Enable smooth scrolling */
scroll-behavior: smooth;
/* Place after the content */
scroll-marker-group: after;
&::scroll-marker-group {
/* Space the markers apart */
display: flex;
gap: 10px;
/* Anchor it to the carousel */
position: fixed;
position-anchor: --carousel;
/* Anchor it horizontally */
justify-self: anchor-center;
/* Anchor it near the bottom */
bottom: calc(anchor(bottom) + 10px);
}
li::scroll-marker {
/* Stylized markers */
content: "";
width: 10px;
height: 10px;
border-radius: 10px;
border: 1px solid black;
}
/* The currently selected marker */
li::scroll-marker:target-current {
background: black;
}
}
- The carousel is responsive (defined by
--carousel-width
). overflow: hidden
also removes the scrollbars (not required).- The required value of
scroll-marker-group
(eitherbefore
orafter
) should match the visual tab order. In this example the scroll markers appear towards the end, which is why we’ve chosenafter
. - You can align/anchor the scroll marker group in whichever way you’d like.
This is essentially the same thing, but vertical, with scrollbars, and with scroll snapping (for the users that are scrolling manually):
Or, the same thing but with incremental numbered markers:
Example: Tabs
This tabs example is very similar, but the differences relate to the more fundamental aspects of the implementation:
scroll-marker-group
is set tobefore
this time, because the tabs are at the top/before the scroll container’s content.- The tab labels are pulled from the HTML data attributes using
content: attr(data-tab-label)
(useful for pulling data from templating values or the back-end).
Browser support
We can detect browser support for it, if needed:
@supports selector(::scroll-marker) {
/* ::scroll-marker supported */
}
@supports not selector(::scroll-marker) {
/* ::scroll-marker not supported */
}
The same thing in JavaScript:
if (CSS.supports("selector(::scroll-marker)")) {
/* ::scroll-marker supported */
}
if (!CSS.supports("selector(::scroll-marker)")) {
/* ::scroll-marker not supported */
}
Specification
The ::scroll-marker
pseudo-element is defined in the CSS Overflow Module Level 5 specification, which is currently in Working Draft status. This means that the information can change between now and the time when it becomes adopted as a formal Candidate Recommendation for browsers to implement.