JavaScript Selection API Table

SelectorSelectsExample
.cardClass - every element carrying it<div class="card">
#headerID - the one element with it<header id="header">
buttonType - every such elementall <button>s
*[lang]Attribute presentany element with a lang attribute
input[type="checkbox"]Attribute equalscheckbox inputs only
nav aDescendant - any depthlinks inside nav, however nested
nav > aChild - one level onlydirect link children of nav
h2 + pAdjacent sibling - immediately afterthe first paragraph following an h2
h2 ~ pGeneral sibling - anywhere afterall following paragraph siblings
a:hover, :focus-visiblePseudo-class - statehovered or keyboard-focused links
p::first-linePseudo-element - sub-partthe first rendered line of each paragraph
li:nth-child(2n)Structural patterneven-numbered list items
Reference: the MDN CSS selectors guide. The reading order that prevents misreads: right to left - the browser filters by the rightmost part first, then checks ancestors, which is why nav a is fine but needlessly long chains are slow to match and brittle to maintain. Specificity climbs in the order type < class < id; the discipline that keeps stylesheets sane is staying in class territory and treating ids as JavaScript hooks, not style targets. Bottom line: the three combinators (space, >, +) are three different promises about structure - pick the weakest one that says what you mean. Related tools: pseudo-elements table and pseudo-classes table for the colon families, CSS animations table for what selectors trigger, and specificity calculator for the arithmetic.

The Selection API reads and drives what the user highlights: window.getSelection() is the global handle, toString() gives the text, selectionchange on document fires on every caret move, and Range objects define boundaries - you can both READ and SET selections programmatically.

Bottom line: character counters, toolbar-at-selection editors and quote-sharing all run on this API. isCollapsed distinguishes a caret from a real span, selectionchange needs debouncing, and the Clipboard API (not the deprecated execCommand) does the copying the selection feeds.

The honest part: selections are per-document singletons with engine quirks - selections inside inputs and textareas do not appear in window.getSelection() on all engines (they have their own selectionStart/End), so form-field selection logic is a separate code path.

How to use

  1. Read the highlighted text: const text = window.getSelection().toString() on selectionchange (debounced ~150ms) - the quote-share and counter feed.
  2. Position a toolbar at the selection: range.getBoundingClientRect() after checking !selection.isCollapsed - the medium-style floating toolbar anchor.
  3. Select programmatically: selection.removeAllRanges(); selection.addRange(range) - quote buttons that pre-select the citable span.

Frequently asked questions

How do I react to selection changes without drowning in events?

selectionchange fires on document for every caret movement - typing, arrow keys, drags - far faster than any UI should react. The pattern is a debounce (150-250ms timer, reset on each event, react when quiet) for toolbar show/hide, or requestAnimationFrame throttling for continuous needs like cursor-position followers. One cross-engine note: on some engines the event does not bubble from inputs, so the listener goes on document - and selections inside input/textarea fields are reported by those elements' own selectionStart/selectionEnd, not by the global selection, on several engines. Form fields are the separate code path.

What is a Range and why does selection code revolve around it?

The boundary object. A Range describes a fragment of the document by start container/offset and end container/offset - it exists independently of any visible selection, which is why it can define, move and measure text spans without the user selecting anything. Selections are essentially ranges the user made: getRangeAt(0) retrieves the first one (multiple ranges exist with Ctrl-based multi-selection, but nearly all UI cares about range 0). Ranges also do their own work: range.getBoundingClientRect() positions UI at the selection, range.selectNodeContents() builds selections to apply, and Range.extractContents() moves the fragment - the vocabulary of every rich-text editor.

Why is isCollapsed the first check in selection UI?

It distinguishes a CARET from a SELECTION. A collapsed selection (start equals end) is just the blinking cursor - there is no highlighted span, so a toolbar that formats or shares text has nothing to act on. Selection-driven UIs check isCollapsed first and hide themselves for carets; forgetting it produces the floating toolbar that appears every time you click. The same check guards copy-and-share handlers: acting on an empty selection copies nothing and looks broken.

How do I select text programmatically and is that ever legitimate?

Build a Range and apply it: selection.removeAllRanges(); selection.addRange(range) - where the range might come from document.createRange() plus selectNodeContents(element), or from locating text. Legitimate uses: pre-selecting the citable portion when the user clicks a quote button (so Ctrl+C copies exactly the right span), selecting generated content after creation, and re-selecting a user's selection after a re-render restores the DOM. The line not to cross: hijacking the selection on page load or mid-read to force attention - selections belong to the user, and programmatic selection is a convenience for THEIR intent, never an attention mechanism.

Related tools