v137 · dom

Word Counter

Select any span of text — including across the ⬡ shadow-DOM inline elements — and get an instant word count, character count, and selection direction. The counter compares what getComposedRanges() sees versus the legacy getRangeAt(0), showing exactly where cross-shadow selections get truncated.

Feature detection: checking…
Document — ⬡ marks an open shadow root

The Selection API gives scripts programmatic access to the user's text selection. Prior to Chrome 137, the that live inside shadow roots were invisible to getRangeAt(0) — you'd get an empty or truncated range.

The new getComposedRanges() method accepts a list of and returns a StaticRange that can cross those roots. The selection's direction property tells you whether the user dragged or backward, independent of the DOM order.

This unlocks real-world use cases: that highlight across component boundaries, accessible read-aloud tools that can speak selected text regardless of encapsulation, and document editors that need precise cursor placement inside custom elements.

Selection Stats
Words
Characters
Characters (no spaces)
Direction none
Cross-shadow
API comparison
getComposedRanges()
getRangeAt(0)
Selection history
Text (truncated) Words Chars Direction Cross-shadow?
Select text to populate history…
// Real-time word counter using composed ranges
document.addEventListener('selectionchange', () => {
  const sel = window.getSelection();
  if (!sel || sel.isCollapsed) return;

  const composed = sel.getComposedRanges({ shadowRoots: openShadowRoots });
  const text = composed[0]?.toString() ?? '';

  const words = text.trim().split(/\s+/).filter(Boolean).length;
  const chars = text.length;
  const dir   = sel.direction; // "forward" | "backward" | "none"

  // Compare with legacy API
  const legacyText = sel.getRangeAt(0)?.toString() ?? '';
  const crossShadow = text.length > legacyText.length;
  // crossShadow === true means getRangeAt missed shadow content
});

see also