The Solution: Text Fragments
Text fragments let you link to any text on any webpage, even if the page owner didn't prepare for it. When someone clicks your link, their browser automatically scrolls to and highlights the text you specified.
A text fragment URL looks like this:
https://example.com/article#:~:text=the text you want to highlightThe magic happens in that #:~:text= part. Everything after it tells the browser what text to find and highlight.
Advanced Options
The basic version just highlights a single phrase, but you can get more specific. Highlighting a range of text from a starting phrase to an ending phrase:
#:~:text=Once upon a time,happily ever afterUsing context clues when the same text appears multiple times on the page. This finds "I was born at Blunderstone" that comes after "Chapter 1-" and before "-A key event", making sure you highlight the right one:
#:~:text=Chapter 1-,I was born at Blunderstone,-A key event Building the Tool
The tool needed to handle two different ways people want to use text fragments:
- Simple mode: Highlight a single piece of text
- Range mode: Highlight from a start point to an end point
Since these modes conflict with each other, I needed the form to be smart about it. If you start typing in the "Main Text" field, you obviously want simple mode, so the range fields get disabled automatically. Vice versa if you use the range fields.
When you click "Generate URL", the tool needs to construct the special text fragment syntax:
generateBtn.addEventListener("click", () => {
const values = {
baseUrl: baseUrlInput.value.trim(),
prefix: prefixInput.value.trim(),
suffix: suffixInput.value.trim(),
text: textInput.value.trim(),
textStart: textStartInput.value.trim(),
textEnd: textEndInput.value.trim()
};
if (!values.baseUrl) return;
const buildFragment = () => {
if (!values.text && !values.textStart && !values.textEnd) return "";
const encode = encodeURIComponent;
const parts = [];
if (values.prefix) parts.push(`${encode(values.prefix)}-`);
if (values.text) {
parts.push(encode(values.text));
} else {
parts.push(encode(values.textStart));
if (values.textEnd) parts.push(encode(values.textEnd));
}
if (values.suffix) parts.push(`-${encode(values.suffix)}`);
return `text=${parts.join(",")}`;
};
const fragment = buildFragment();
outputUrlSpan.textContent =
values.baseUrl + (fragment ? `#:~:${fragment}` : "");
visitBtn.disabled = false;
visitBtn.classList.remove("sr");
});
Important parts:
- URL encoding: Special characters like spaces get converted to %20. The encodeURIComponent() function handles this.
- Comma separation: When using start and end text, they're separated by a comma
- Hyphens for context: Prefix uses prefix- and suffix uses -suffix with hyphens
- Empty check: If no text is entered, just return the base URL without modification
In the end we have two buttons, to copy the url and to open in a new tab. An example of the copy code (stolen from somewhere , thanks Dan Stevens!):
copyBtn.addEventListener("click", () => {
const urlToCopy = outputUrlSpan.textContent;
if (urlToCopy) {
const tempTextArea = document.createElement("textarea");
tempTextArea.value = urlToCopy;
document.body.appendChild(tempTextArea);
tempTextArea.select();
try {
document.execCommand("copy");
} catch (err) {
console.error("Failed to copy text: ", err);
} finally {
document.body.removeChild(tempTextArea);
}
}
});