Deep linking beyond IDs

Fragment identifiers have long enabled deep links to specific parts of a page — but only if the target element happens to have an id attribute. That constraint breaks down quickly on pages where IDs are sparse or absent entirely. Text Fragments remove that requirement by letting the URL itself carry the text you want to point at.

When Chrome 80 shipped, its release announcement on the Chromium blog showcased several new features. Inspecting that page’s markup revealed a familiar situation: only elements with explicit id attributes were deep-linkable. A quick DevTools snippet highlighted those elements, and each one could be targeted with a URL like https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1.

// Example of parsing such a URL in JavaScript
const url = new URL('https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#HTML1');
console.log(url.hash); // "#HTML1"

The problem is practical: finding a linkable id typically requires opening DevTools, and most content on the web was never authored with such anchors in mind. Headings often lack id attributes entirely, making them unreachable via traditional fragment links.

How text fragments work

Text Fragments encode the target text directly in the URL fragment. When such a URL is opened, the browser locates the matching text on the page, scrolls it into view, and visually emphasizes it for the user. This works for any text content — headings, paragraphs, list items — without requiring any site changes or id attributes.

The format follows a simple pattern: after the standard fragment delimiter, the text fragment syntax specifies the start and optionally the end of the desired passage. The browser handles the matching and emphasis automatically.

Use cases

The feature is most useful for linking to content that was never designed to be linkable. Common scenarios include:

  • Quoting a specific sentence from a long article
  • Pointing colleagues to a particular error message or code snippet
  • Sharing a section of a spec or documentation page that lacks anchor IDs

This eliminates the need for page authors to predict every possible deep link target or retrofit IDs onto existing content. It also complements existing fragment-based navigation: when an id is available, the browser still honors it; when it isn’t, the text fragment fills the gap.

Scope and compatibility

As of Chrome 80 and later versions, Text Fragments are available for pages that load over secure contexts. This means the feature works on HTTPS pages but not on insecure origins. The syntax is accepted by browsers that support the feature, while other browsers simply ignore the fragment and load the page normally.

Linking straight to the sentence you need

The Text Fragments proposal lets you specify a text snippet directly in the URL hash. When a browser that supports the feature navigates to such a URL, it emphasizes the matching text and brings it into view. Browser support spans Chrome, Edge, and Safari, with Firefox support arriving later.

For security reasons, the feature only works when links are opened in a noopener context. Ensure your <a> markup includes rel="noopener", or add noopener to the window features list in your Window.open() call.

The start parameter

In its simplest form, the syntax is the hash symbol # followed by :~:text= and the percent-encoded text you want to link to. For instance, linking to the ECMAScript Modules in Web Workers heading in the Chrome 80 announcement blog post would look like:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers

In a supporting browser, the text fragment gets highlighted and scrolls into view.

Linking a range with start,end

What if the target is an entire section rather than just a heading? Percent-encoding all of the section's text would result in an impractically long URL. Instead, use the start,end syntax: specify a few words from the beginning of the snippet and a few words from its end, separated by a comma. To highlight the full section following that heading, the URL would be:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules%20in%20Web%20Workers,ES%20Modules%20in%20Web%20Workers.

A shorter URL with only two words on each side would also work:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript%20Modules,Web%20Workers.

However, if you bring it down to one word per side, you run into trouble:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=ECMAScript,Workers.

This URL is shorter, but the highlight stops at the first occurrence of the word Workers. The preview is correct, but the section is not uniquely identified by these short start and end values.

Disambiguating with prefix- and -suffix

Longer start and end values are one route to uniqueness. But it's not always possible to extend them, particularly when the target is a single word. For example, consider the word text in the Chrome 80 blog post, which appears four times. The fourth occurrence is a code-styled word, and the third is preceded by "the" and followed by "parameter." A URL like #:~:text=text would only match the first occurrence of the word "Text" in the heading.

The prefix- and -suffix parameters solve this. They're followed and preceded by a dash - to delimit them from start and the optional end. Since no other occurrence of the word "text" has the surrounding words "the" and "parameter," you can pinpoint it like so:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=the-,text,-parameter

Full syntax and block-level matching

The complete syntax is #:~:text=[prefix-,]start[,end][,-suffix]. Square brackets indicate optional parameters, and all values require percent-encoding. This is particularly important for the dash -, ampersand &, and comma ,, so they aren't parsed as syntax.

A single match for prefix-, start, end, or -suffix can only occur within one block-level element. Full start,end ranges, however, may span multiple blocks. For instance, :~:text=The quick,lazy dog would fail if "The quick" appears across two different block-level elements, but would succeed if it's contained within a single block.

Hand-crafting these URLs, especially guaranteeing uniqueness, is tedious. The Link to Text Fragment extension automates the process. After selecting text, you click "Copy Link to Selected Text" in the context menu. The extension is available for Chrome, Edge, Firefox, and Safari.

Multiple fragments and fallbacks

A URL can hold multiple text fragments, separated by an ampersand character &. An example with three fragments would look like:

https://blog.chromium.org/2019/12/chrome-80-content-indexing-es-modules.html#:~:text=Text%20URL%20Fragments&text=text,-parameter&text=On%20islands,%20birds%20can%20contribute%20as%20much%20as%2060%25%20of%20a%20cat's%20diet

Text fragments can also coexist with traditional element fragments in the same URL. This is useful as a fallback: if the page content later changes and the text fragment no longer matches, the element fragment can still direct the user to the general area.

The fragment directive

The syntax element :~: is the fragment directive. It's reserved for user agent instructions such as text= and is stripped from the URL during loading so that author scripts can't directly interact with it. This isolation is intentional to allow for future additions, like translation hints, without breaking existing content.

To detect support, test for the read-only fragmentDirective property on document. Detection is primarily useful for dynamically generated links, such as those from search engines, to avoid serving unsupported URLs to older browsers.

Styling and accessibility

Browsers style text fragments like the mark element by default, typically as black text on a yellow background. The ::target-text pseudo-selector exposes this for customization, allowing you to pick different colors. Always ensure that your override maintains adequate contrast and visually distinguishes the highlighted text from the rest of the content.

Polyfilling and programmatic generation

A polyfill provides partial support in JavaScript for browsers without native implementation, and is used internally by the Link to Text Fragment extension. The polyfill also includes a utility file, fragment-generation-utils.js, which you can import to programmatically generate Text Fragment links.

Analytics

Because browsers strip Text Fragments to prevent interference with fragment-based routing, there's no official way to access them in page scripts. Although there's an acknowledged need to expose them for analytics, no solution is implemented. As a workaround, you can extract the information from the URL before navigation, or use a code snippet that inspects the URL early in the page lifecycle.

Security and privacy constraints

Text fragment directives only run on full (non-same-page) navigations triggered by a user activation. Cross-origin navigations require a noopener context so the destination is isolated. Directives apply exclusively to the main frame; iframe content is not searched, and iframe-initiated navigations don't trigger them.

Privacy protections prevent implementations from leaking whether a match occurred. Since text fragments can come from anyone, a malicious ad network could craft a hidden cross-origin iframe with a URL like dating.example.com#:~:text=Log%20Out to determine a victim's logged-in state. The design prevents such scenarios by not focusing or scrolling on a match when it would reveal the result to another frame.

Scroll-on-navigation was assessed for risk by the Chrome security team. While it could theoretically be leveraged in highly specific network traffic analyses, it was deemed manageable, and other user agents may opt for a manual scroll UI instead. For sites wishing to opt out entirely, Chromium supports a Document Policy header that instructs user agents to skip processing of Text Fragment URLs.

Opting out of text fragments

If you don’t want the browser to honor this link format, an extension such as ModHeader (a third-party product) can inject an HTTP response header. The header must be added to the response, not the request:

Document-Policy: force-load-at-top

Alternatively, Chrome Enterprise users can disable the feature through the ScrollToTextFragmentEnabled policy documented in the Chrome Enterprise policy list. On macOS, the policy can be applied from the terminal:

defaults write com.google.Chrome ScrollToTextFragmentEnabled -bool false

For Windows, see the setup instructions on the Google Chrome Enterprise Help site.

How search engines use them

When a Google search yields a featured snippet, the snippet text is itself the link destination. Clicking it takes you to that passage inside the source page — the URL contains a text fragment that was generated automatically by the search engine.

Google Search results page showing a featured snippet. The status bar shows the Text Fragments URL.
After clicking through, the relevant section of the page is scrolled into view.

Why this matters

Text fragment links give anyone a precise way to point readers at a specific part of a page. That makes them valuable for academic citations, for deep linking from search results, and for sharing a quote from a page without falling back to a screenshot. The Link to Text Fragment extension will create such links for you.

History and acknowledgements

The feature was specified and implemented by Nick Burris and David Bokan with contributions from Grant Wang. Joe Medley provided a thorough review of this document. The specification draft, design review, Chromium tracking bug, and the various standards-position threads are linked from the related resources at the bottom of the original article.