Redefining Scrollspy with CSS (No JS Needed!)
Are you familiar with the Scrollspy UI pattern?
You might be thinking: “Duh! Of course I am! Who isn’t?”
I don’t know about you, but I’ve only recently learned that the “highlight the active link when its target section scrolls into view” pattern is what designers refer to when they talk about the Scrollspy effect. I even did some googling and found out that the name originates from Bootstrap, which has a documentation page dedicated to this pattern.
Anyway…
The Scrollspy effect is based on the idea that you “spy 🕵️” on sections of content on a page (using JavaScript and something like the Intersection Observer API), and when a specific part or section of the content scrolls into view, you visually highlight the link to that section. This pattern is most commonly implemented in one-page websites with a fixed header and a nabber (like the live example in the Bootstrap documentation), or as sticky Tables of Content on article pages, like the one on MDN, for example.
Here’s a video of an MDN article page showing the links in the sticky table of contents on the right side of the article get highlighted as you scroll through the sections in the article.
Historically, we’ve had to use JavaScript to create this effect. Typically it involved adding a .active
class name to the active link.
If you inspect the links on the MDN website, you’ll notice that the highlight styles are applied to a link when the link gets an aria-current=true
attribute. This attribute is added via JavaScript and used as a styling hook in CSS to style the active link. Here’s a screenshot showing the aria-current
attribute on the active link visible in the Elements panel of the browser DevTools.

Using ARIA attributes as styling hooks is one of my favorite ways to use ARIA attributes while enforcing accessibility requirements in projects.
Today, it is possible to recreate the Scrollspy effect without JavaScript.
Using a new experimental CSS feature, the browser (currently only Chrome) will do the “spying” for you (no, I’m not referring to the privacy issues here 😜) when you use the new property: scroll-target-group
.
The scroll-target-group
property has recently been added to the CSS Overflow Module Level 5.
If you’ve read my previous article examining the accessibility of "CSS Carousels", then you’re already familiar with this specification (and you, too, may have very strong opinions and feelings about it 🤗). If you haven’t read the article, I highly recommend reading it as it will give you some relevant context for the technical discussion in this article.
The scroll-target-group
property is similar in concept to the scroll-marker-group
property, with one important difference:
The scroll-marker-group
is used to generate CSS scroll markers in the form of CSS pseudo-elements. The ::scroll-marker
s and their containing element are generated by the browser from your CSS. The CSS-generated markers come with certain behavior built into them provided by the browser, and the active scroll marker within the scroll marker group can be styled using the :target-current
pseudo-selector. (This implementation currently has unresolved accessibility issues that I discussed in the CSS Carousels accessibility article.)
The scroll-target-group
property, on the other hand, is meant to be used on an HTML container element, and is used to enrich HTML anchor elements functionality to match the pseudo elements one, which makes it possible to use the :target-current
selector to highlight links when their respective targets are in view. 🙌🏻
In other words, scroll-target-group can be used to promote ‘regular’ HTML anchor elements (<a href="">
) to become scroll markers. The browser will then use a specific algorithm to determine which anchor is the active anchor within the group, just like it determines the active marker within a group of CSS ::scroll-marker
s. The active anchor then matches the :target-current
selector, which you can use to visually highlight the active link.
All you need to use this feature is to start with semantic markup for the group of anchors. For example, you might start with a <nav>
landmark region containing an ordered list of links to sections within an article:
<nav aria-labelledby="toc-label">
<span id="toc-label" hidden>Table of Contents</span>
<ol role="list">
<li><a href="#one">Section One</a></li>
<li><a href="#two">Section Two</a></li>
<li><a href="#three">Section Three</a></li>
<li><a href="#four">Section Four</a></li>
<li><a href="#five">Section Five</a></li>
</ol>
</nav>
Then, you instruct the browser to treat these links as scroll markers by using the scroll-target-group property on the list:
nav[aria-labelledby=toc-label] {
scroll-target-group: auto;
}
Now when a target section is scrolled into view, the browser will determine the active link in the group, which will match the :target-current selector and apply the active link styles to it:
a:target-current {
font-weight: bold;
text-decoration-thickness: 2px;
}
It’s literally as simple as that. Of course, you’d have additional styles in your stylesheet that make the table of contents stick to the top of the viewport or whatever as the user scrolls the page.
Here’s a video recording of the CSS Scrollspy in action:
I’ve discussed this feature in more depth and created a live example for you to tweak at in an article I just published on my blog. You will need to view the live example in Chrome (140+) to see it working. I recommend you give the blog post a read when you can because in it I also discuss considerations for styling the active links accessibly, and I also mention how :target-current compares to :target when styling the document target element on page load.
This feature is a nice little progressive enhancement that you can add to your designs… when it becomes ready for production. Unfortunately, as I wrote in the blog post, this feature is currently not entirely accessible.
When the browser determines the active link within the group, it is supposed to add aria-current=true to the active link—similar to what MDN does. A scroll marker, by definition, has a meaningful purpose: it lets the user know which part of content is currently being viewed.
What this means is that when you visually highlight an active link, you’re communicating meaningful information to the user. You must ensure that the same information is communicated to all users, including screen reader users, to ensure that you are not excluding anyone out. This is a baseline accessibility requirement.
WCAG Success Criterion 1.3.1 Info and Relationships (Level A) states that information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text.
Because the scroll-target-group
property and the :target-current
selector are designed to allow us to create JavaScript-free native HTML scroll markers, we should expect the browser to add and manage the necessary ARIA attribute(s) required for scroll markers to be inclusive. (After all, that’s the whole premise of this feature: to write a few lines of CSS and let the browser handle all the behavior for us.)
However, at the time of writing, Chrome (which is the only browser that currently supports this feature) doesn’t add aria-current=true
to the active anchor yet. I filed an issue for this.
If you want to use this feature today, keep in mind that you will, for the time being, need to use JavaScript to add aria-current
to the active anchor when its corresponding target scrolls into view, otherwise you risk an instant WCAG 1.3.1 violation.
I’ll personally wait till the issue is resolved and the feature becomes ready for production. I will update my blog post when the issue is resolved. When that happens, I’ll be among the first to add it as an enhancement in my CSS. ✌🏻 And that’s it for this issue!
In the next issue, I will be discussing another CSS feature—one that allows you to improve the usability and accessibility of your content. See you next week!