You Don't Need JavaScript to Highlight Hash Targets
Style the exact text a URL fragment resolves to. No JavaScript, no class toggling. Just CSS on real navigation.
What Happens When Visual Feedback Is Minimal
Having a table of contents on a page full of rich text is a pretty common pattern, one exists on this page for the sheer ease of being able to jump through the content more decisively. Each item in the contents list jumps to a heading on the page instantly. Not particularly groundbreaking. But the ‘jump’ is the only feedback that’s typically given. Was it this heading I’ve gone to? the paragraph below it? the whole section? The ‘jump’ could be missed; scrolling after the click, navigating with a screen reader (where the scroll position isn’t announced) or merely blinked, then that context is gone in an instant.
The usual fix has been to add extra feedback with Javascript, typically having a scroll listener watching for hash changes which toggles a highlight class on whatever element matches. Which works…but it’s extra weight to deal with something the browser already knows. It’s got the info on what text a hash link points to, because it just sent you there. It’s also a brittle solution most of the time. An edge case gets missed, like a hash that doesn’t map to an id, nested headings or content that loaded after the listener initialised, and the highlight just doesn’t fire.
::target-text sidesteps all of that to use the case it’s built for. A real navigation to a real destination on the page. No listener, no class toggling, no JS to maintain. The browser handles the targeting and you just style what it hands you.
What ::target-text Actually Does
::target-text is a pseudo-element that styles the exact span of text a URL’s text fragment resolves to — the part after #:~:text= in a link e.g. /writing/target-text/#:~:text=Where%20It%20Shines. Click this, and the browser doesn’t just scroll to where “Where It Shines” appears, but also hands you a styleable hook onto that specific text, live in the page you landed on. Unsurprisingly, this also works for external URLs as well.
::target-text {
background-color: var(--color-feature);
color: var(--color-dark);
}
That’s the whole API. We’re not including JavaScript and querying the DOM for a match. The browser has already done the matching as part of resolving the navigation, and ::target-text is just where you style the result.
:target. The difference: :target matches an element — specifically, whichever element has an id matching the URL’s fragment (#key-features). ::target-text matches text e.g. a phrase the browser located via the text fragment syntax, with or without any id in sight. You can point to specifics in content, like “the third sentence in a paragraph that has no heading, no anchor, no id at all,” which :target was never able to do. They can coexist on the same page targeting completely different things.The part that matters most for how you use this: it only activates on an actual navigation. Following a link with a text fragment attached, or a browser typing/pasting a URL directly, triggers it. Changing the hash via JavaScript on a page that’s already loaded, or highlighting text some other way client-side, does not. For that we would need the Custom Highlight API.
Where It Shines
Navigation — jump to “Browser Support & Caveats” and the exact phrase lights up Readability — removes ambiguity about which content a link was “about” Accessibility — visible landing point for keyboard/AT users, not just a scroll position
Browser Support & Caveats
This one’s Baseline as ‘Newly available’ as of December 2024. Full modern support, but recent enough that older browser versions may not have it. For anything from before then, there’s no breakage here - links still work as expected, we just don’t get that highlighting. We could depend on that brittle scroll listener solution, or we could just expect there to be no highlighting. It’s a small win here, just a little nicety.
Try It Yourself
The easiest way to feel what this actually does is to try it on something you already have. If your site has any kind of table of contents, footnote list, or “jump to” navigation, grab one of those links and see what happens when you add a text fragment styled with ::target-text at the other end. You’ll notice the difference in landing feel almost immediately, especially on a page long enough that the destination isn’t obviously the top of the viewport.
A couple of things worth trying deliberately; point a text fragment at a paragraph with no heading or id nearby — confirms it’s really matching text, not riding on :target. Then try it in a browser where support isn’t there yet, and confirm nothing breaks.
If you’re building something where the highlight needs to respond to user input rather than a URL — search results, live filtering, anything client-side and dynamic — that’s outside what ::target-text can do by design, since it only ever fires on navigation. Instead, that’s a job for the CSS Custom Highlight API, which is a related piece of the same puzzle, but different enough in how it works that it deserves its own post rather than a rushed footnote here. I’ll be writing about it properly once I’ve built something worth showing rather than just describing the API secondhand. Until then, MDN’s overview is the best place to start if you want to get ahead of it yourself.
For now, ::target-text is a small, quiet win.