Build a "Quote This" Feature with the Selection API
Users already highlight text while reading - it's practically a nervous tic. Sites like Medium turned that into a feature: select a sentence, get a little "share" button. The machinery is one global object and one event, and it's been in browsers forever.
Reading what's selected
document.addEventListener('selectionchange', () => {
const selection = document.getSelection();
const text = selection.toString().trim();
if (text.length < 10 || selection.isCollapsed) {
hideQuoteButton();
return;
}
showQuoteButton(text, selection);
});
selectionchange fires on every adjustment (it's chatty - debounce it in real code), and isCollapsed distinguishes an actual selection from a plain cursor click. The length check avoids popping UI over accidental double-clicks.
Positioning the button over the selection
The selection's Range object knows its own geometry:
function showQuoteButton(text, selection) {
const rect = selection.getRangeAt(0).getBoundingClientRect();
btn.style.left = `${rect.left + rect.width / 2 + scrollX}px`;
btn.style.top = `${rect.top + scrollY - 44}px`; // float above
btn.hidden = false;
btn.onclick = () => {
const quote = `"${text}" - ${document.title} ${location.href}`;
navigator.clipboard.writeText(quote);
toast('Quote copied');
};
}
getBoundingClientRect on the range gives viewport coordinates; adding scrollX/scrollY converts to page coordinates for an absolutely positioned button. Multi-line selections return the union box, which is usually what you want for centering.
Details that separate polished from janky
- Scope it: check that the selection lives inside your article element (
article.contains(selection.anchorNode)) so the button doesn't appear when someone selects nav text. - mouseup vs selectionchange: showing UI on selectionchange means it flickers during the drag. The classy pattern: track validity on selectionchange, but reveal the button on mouseup/touchend, when the gesture is finished.
- Don't steal the selection: if your button takes focus on mousedown, the selection collapses before the click handler reads it.
btn.addEventListener('mousedown', e => e.preventDefault())keeps the highlight alive. - Share targets: on mobile, navigator.share({ text: quote }) hands off to the native share sheet - feature-detect and prefer it over clipboard.
Same API powers the other direction too: prefilling a feedback form with the selected text ("report a typo" features), or checking selection.toString() in a copy event listener to append attribution. Ethical note on that last one: readers hate sites that mangle their clipboard - append, never replace, and keep it short.
Support: universal, including the touch-selection handles on mobile. One of those APIs that's been quietly complete for a decade, waiting for product ideas.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.