completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Selection & Range

Learn how Range objects mark pieces of the DOM, how the Selection API reads what a user selected, how text box selection differs, and how to build a safe highlighter.

You’ll learn to
  • 01
    Mark an exact part of the pageMake a Range with a start point and an end point. Each point is a node and an offset.
  • 02
    Read what the user selectedCheck that a selection exists, then read its text, its anchor, its focus, and its Range.
  • 03
    Change selected text safelyMove the cursor in a text box, and wrap selected page text in a mark element.

What does "selected text" mean to JavaScript?

When you drag the mouse across words on a page, the browser colors them blue. It also remembers where the selected part starts and where it ends. JavaScript can read those two places. Then it can copy the selected text, delete it, or wrap it in a new element. Quote buttons, comment tools, and text editors work this way.

This lesson covers three tools. Their names sound alike, but each one has its own job:

  • A Range marks one exact part of the page. It has a start point, an end point, and everything between them.
  • The Selection is what the user has selected on the page right now. It holds a Range. It also remembers which way the user dragged.
  • Input and textarea selection is separate. A text box keeps its own cursor position as numbers, such as selectionStart.
The main idea

A Range marks one part of the page. The Selection is what the user selected right now. A text box keeps its own cursor, as two numbers.

Real-life analogyA Range is two bookmarks in a book

You put one bookmark before a sentence and one after it. The book does not change. But now you can say "copy these pages" or "tear out these pages". A Range gives JavaScript the same two bookmarks inside a web page.

In real life: The first bookmark
In JavaScript: The start point: startContainer and startOffset
In real life: The second bookmark
In JavaScript: The end point: endContainer and endOffset
In real life: The pages between the bookmarks
In JavaScript: The marked part: range.toString() gives its text
In real life: The chapter that holds both bookmarks
In JavaScript: commonAncestorContainer: the smallest node that holds both points

You met nodes in the DOM tree lesson. A node is one piece of the page: an element such as <p>, or a piece of text inside it. You also learned to add and remove whole elements. This lesson works on a smaller piece: the exact words a reader selected.

A Range: a start point and an end point

INTERACTIVE

You make a Range with document.createRange(). A new Range is empty. You give it a start point with setStart(container, offset) and an end point with setEnd(container, offset). Each point needs two things:

  • The container: the node the point sits in.
  • The offset: a number that says how far into that node the point is.

What the offset counts depends on the kind of node. Read each row of the table as: "when the container is this kind of node, the offset counts these things."

What the offset counts
Kind of containerWhat the offset countsExample
A text node, which is a piece of text.UTF-16 code units: the small pieces JavaScript uses to store a string.In "A😀B", the offsets are 0, 1, 3, and 4. The emoji 😀 takes two code units.
An element node, such as <p>.Child nodes: the nodes directly inside the element.In <p>Hello <b>bold</b>!</p>, offset 1 is after the text Hello and before the <b>.

A UTF-16 code unit is how JavaScript counts the length of a string. Most letters take one code unit. Many emoji take two. The Unicode & string internals lesson explains why. A Range counts the same way. It does not count the symbols you see on the screen.

A Range inside one text nodePop out in the code editor (opens in a new tab)JavaScript
const text = document.createTextNode("tea");const range = document.createRange();range.setStart(text, 0);range.setEnd(text, 3);console.log(range.toString(), range.collapsed);

Line 1 makes a text node that holds "tea". Line 2 makes an empty Range. Line 3 puts the start point at offset 0, before the t. Line 4 puts the end point at offset 3, after the a. Line 5 prints tea false:

  • range.toString() gives the text between the two points: tea.
  • range.collapsed is true only when the start and end are in the same place. Here they are not, so it is false.
The text tea as three letter cards, t, e, and a. Offsets 0, 1, 2, and 3 sit between the letters. A start tag points at offset 0 and an end tag points at offset 3, so range.toString() gives tea.startoffset 0teaendoffset 30123toString() → "tea"
Offsets sit between letters, not on them. Offset 0 is before t and offset 3 is after a, so the Range holds all of "tea".
Offsets have limits

A text offset can't be bigger than the length of the text. An element offset can't be bigger than the number of child nodes. If it is, JavaScript throws an IndexSizeError. So read the limit from the node, for example its length. Don't guess it.

Range builder: move the start and end points
Range builder codePop out in the code editor (opens in a new tab)JavaScript
const root = document.querySelector("#demo");const range = document.createRange();range.setStart(root.childNodes[0], 6);range.setEnd(root.childNodes[2], 5);console.log(range.toString());getSelection().removeAllRanges();getSelection().addRange(range);
Live DOMTry offsets 0–10
range.toString()
—
start
—
end
—
collapsed
—
commonAncestorContainer
—
Try it yourself
Start point

Text offsets count UTF-16 code units. Valid offsets: 0–6.

End point

Text offsets count UTF-16 code units. Valid offsets: 0–10.

Show the Range as

Choose a start point and an end point to make a Range.

This paragraph is built by plain DOM code inside an empty box, so React never touches the nodes the Range marks.

Try three things in the Range builder:

  • Put the start and the end at the same place. The Range is now collapsed: it has no text, only a position.
  • Choose root element as the container. Now the offset moves one child node at a time.
  • Choose a text node as the container. Now the offset moves one code unit at a time.

The second button uses the CSS Custom Highlight API. It is a newer browser tool that colors a Range without changing the page. If your browser doesn't have it, the demo uses the normal Selection instead.

The Selection: what the user selected

INTERACTIVE
Real-life analogyThe Selection is a highlighter pen in the reader's hand

A reader presses a highlighter pen on the page, drags it, and lifts it. The browser remembers both ends of that drag. If the reader drags backwards, the same words get colored. But the place where the pen touched down and the place where it lifted off swap.

In real life: Where the pen first touched the page
In JavaScript: The anchor: anchorNode and anchorOffset
In real life: Where the pen lifted off
In JavaScript: The focus: focusNode and focusOffset
In real life: The colored words
In JavaScript: selection.toString() and getRangeAt(0)
In real life: The pen touched down but did not move
In JavaScript: selection.isCollapsed === true: a cursor, no text

window.getSelection() and document.getSelection() both give you the page's Selection. A Selection usually holds zero Ranges or one Range. rangeCount tells you how many. Check it before you call getRangeAt(0). If there is no Range yet, that call throws an error.

Two words name the ends of the user's drag:

  • The anchor is where the drag started.
  • The focus is where the drag ended.

Newer browsers also have selection.direction. It says "forward" or "backward". Check that it exists before you use it.

Live selection inspector
Selection inspector codePop out in the code editor (opens in a new tab)JavaScript
document.addEventListener("selectionchange", () => {  const selection = document.getSelection();  if (!selection || selection.rangeCount === 0) return;   const range = selection.getRangeAt(0);  if (!demo.contains(range.commonAncestorContainer)) return;   console.log(selection.toString());  console.log(selection.anchorNode, selection.anchorOffset);  console.log(selection.focusNode, selection.focusOffset);});
Try selecting this textReal selectionchange

Start at either end. A backwards drag selects the same words, but the anchor and the focus show where your drag started and ended.

selection.toString()
Select some words in the paragraph.
anchor
—
focus
—
rangeCount
0
isCollapsed
true
type
None
direction
Checking…
Try it yourself

Drag from left to right, then from right to left, over the same words.

The listener is on document, so it hears every selection on the page. The code ignores any Range outside the demo paragraph.

If you click somewhere else on the page, the Selection changes. This is the browser's real Selection.

Select a word from left to right. Then select the same word from right to left. The selected text is the same both times. The Range's start and end are the same too, because a Range always keeps its start before its end. But the anchor and the focus swap. Text editors use this when you hold Shift and press an arrow key to grow a selection.

Left: a drag from left to right over the selected words. The anchor is at the left end and the focus is at the right end. Right: a drag from right to left over the same words. The anchor is at the right end and the focus is at the left end.drag left to rightselected wordsanchorfocusdrag right to leftselected wordsfocusanchor
The same words are selected both times. Only the anchor (where the drag started) and the focus (where it ended) swap places.
Check for an empty SelectionPop out in the code editor (opens in a new tab)JavaScript
const selection = document.getSelection();selection.removeAllRanges();console.log(selection.rangeCount, selection.toString());

Line 1 gets the Selection. Line 2 removes every Range from it, so nothing is selected. Line 3 prints 0 and an empty string. This is why your code should check rangeCount before it calls getRangeAt(0).

Keep the selectionchange code small

The browser fires a selectionchange event each time the selection changes. While the user drags, that can happen many times a second. So keep the code that runs small. Ignore selections outside your part of the page. Remove the listener when you no longer need it. Don't rebuild the page on every event.

Selection inside inputs and textareas

INTERACTIVE
Real-life analogyA text box has its own cursor

A text box is like a tiny text editor. Its cursor lives inside the text in the box. So the box tells you plain numbers, such as "the cursor is after letter 2".

In real life: The blinking cursor
In JavaScript: selectionStart === selectionEnd
In real life: Some highlighted letters
In JavaScript: From selectionStart up to selectionEnd
In real life: Typing over the highlighted letters
In JavaScript: setRangeText(text, start, end, mode)

For <input> and <textarea>, you don't need nodes. Use the box's own tools:

  • selectionStart and selectionEnd: where the selected letters start and end. These are indexes, counted from 0.
  • selectionDirection: "forward", "backward", or "none".
  • select(): selects all the text in the box.
  • setSelectionRange(start, end): selects the letters from one index to another.
  • setRangeText(text, start, end, mode): puts new text in place of the letters from start to end.

Don't use a page Range for the text inside a text box. Browsers handle that in different ways. The box's own tools are made for this job.

Textarea cursor lab
Textarea selection codePop out in the code editor (opens in a new tab)JavaScript
const box = document.querySelector("textarea"); box.select();box.setSelectionRange(2, 5, "forward");box.setRangeText("😀", box.selectionStart, box.selectionEnd, "end"); console.log(box.selectionStart);console.log(box.selectionEnd);console.log(box.selectionDirection);
TextareaIts own selection
selectionStart
0
selectionEnd
0
selectionDirection
none
Try it yourself

These numbers come from the textarea itself, not from a page Range.

You can also click or type in the textarea. The numbers update each time the selection or the text changes.

The third button calls setRangeText("😀", start, end, "end"). If no letters are selected, it puts the emoji in at the cursor. If some letters are selected, it replaces them. The last argument, "end", moves the cursor after the emoji. Emoji buttons, @mention pickers, and autocomplete use this.

Put the cursor at the endPop out in the code editor (opens in a new tab)JavaScript
const box = document.querySelector("#caret-demo");box.focus();box.setSelectionRange(box.value.length, box.value.length);console.log(box.selectionStart);

The blinking cursor in a text box is also called the caret. Line 1 finds the textarea. Line 2 focuses it, so the caret shows. Line 3 uses the length of the text for both the start and the end. When the start and end are equal, no letters are selected; there is only a caret. So the caret sits after the last letter. The text "Type here" has 9 characters, so line 4 prints 9.

Put text in at the caretPop out in the code editor (opens in a new tab)JavaScript
const box = document.querySelector("#caret-demo");box.setSelectionRange(2, 2);box.setRangeText("!", box.selectionStart, box.selectionEnd, "end");console.log(box.value, box.selectionStart, box.selectionEnd);

Line 2 sets the start and the end both to 2. So the caret is after "Ty", and no letters are selected. Line 3 puts ! in at the caret and replaces nothing. The "end" mode moves the caret after the !. Line 4 prints Ty!pe here 3 3.

Before: the textarea holds Ty, then the caret, then pe here. Start and end are both 2. After setRangeText with an exclamation mark, it holds Ty!, then the caret, then pe here. Start and end are both 3.setRangeText("!", 2, 2, "end")beforeTy|pe herestart 2, end 2afterTy!|pe herestart 3, end 3
The | is the caret. With start and end both 2, nothing is replaced: the ! goes in at the caret, and the caret moves after it, to 3.

Build a highlighter

INTERACTIVE

A highlighter wraps the selected words in a <mark> element. Browsers show the text inside <mark> with a yellow background. The steps are:

  1. Get the current Selection.
  2. Get its first Range.
  3. Check that the Range is inside the part of the page your code owns.
  4. Wrap the Range in a <mark>.

Step 3 matters. Without it, a button in your widget could change text somewhere else on the page, such as the menu or this lesson's own text.

Build a highlighter
Highlighter codeJavaScript
const selection = document.getSelection();const range = selection.getRangeAt(0);if (!container.contains(range.commonAncestorContainer)) return; const mark = document.createElement("mark");try {  range.surroundContents(mark);} catch {  mark.append(range.extractContents());  range.insertNode(mark);} container.querySelectorAll("mark").forEach((mark) => {  mark.replaceWith(...mark.childNodes);});container.normalize();
Demo textTry selecting into the bold words
Try it yourself

Select some words in the paragraph, then press Highlight selection.

The demo only changes its own box. It ignores selections in the rest of this page.

The short way to wrap is range.surroundContents(mark). It works when the Range holds only whole elements. It throws an error when the Range cuts an element in half. For example, start a selection in plain text and end it in the middle of the bold words. Then the code uses a longer way:

  1. range.extractContents() takes the selected part out of the page.
  2. mark.append(...) puts that part inside the mark.
  3. range.insertNode(mark) puts the mark back where the selected part was.
Only change your own part of the page

Only change the part of the page your code made. In these demos, the text that gets changed is built by plain DOM code inside an empty box. React, the tool that draws this page, never touches that box. So React and your code never change the same nodes.

Don't wrap the same words twice

A user can press Highlight twice on the same words. Decide what should happen then: nothing, remove the mark, or replace it. Otherwise each press puts another <mark> inside the last one, and the text gets harder to edit later.

Range methods that copy, remove, and insert

STEP THROUGH

A Range has methods. Some of them move the Range's points:

  • selectNode(node) marks a whole node, tags and all.
  • selectNodeContents(node) marks only what is inside the node.
  • collapse(true) moves the end point to the start point. collapse(false) moves the start point to the end point.
  • cloneRange() makes a new Range with the same start and end. Use it to save a position for later.

Other methods work on the content between the two points. In the replay below, each method starts with the same Range around the word magic. Pick a method and step through it. Before each step, guess: does the paragraph change?

Range methods: copy, remove, take out, put in
Step 0 of 7Ready
Your turn: follow the blue line

A Range has a start point and an end point. Switch the method and guess which ones copy, remove, take out, or put in content.

Running in
  1. script
Next: line 1
const text = paragraph.firstChild;const range = document.createRange();range.setStart(text, 6);range.setEnd(text, 11);const copy = range.cloneContents();console.log(copy.textContent);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick a method. The Range always marks the word magic first.

Changing the method starts the replay again, with the same start and end points.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

cloneContents() and extractContents() both give back a DocumentFragment. A DocumentFragment is a small group of nodes that is not on the page.

Left: cloneContents. The paragraph still says Range magic works, and a copy of magic comes back. Right: extractContents. The paragraph now says Range, two spaces, works, and the word magic comes back.cloneContents()Range magic workspage stays the samemagica copyextractContents()Range  worksmagic is gonemagictaken out
Both methods give back magic. cloneContents() leaves the paragraph as it was; extractContents() takes the word out of it.

Where you'll use this

These tools show up in anything that works like a text editor:

  • Quote buttons that copy the selected words.
  • Comments pinned to a few words in an article.
  • Rich-text editors that make the selected words bold or turn them into a link.
  • Highlighters and note tools.
  • Search tools that color the words they found.

The steps are almost always the same: read the Selection, check it, then turn it into what your feature needs.

Quote the selected text from an articleJavaScript
const selection = document.getSelection();if (!selection || selection.rangeCount === 0) return;const range = selection.getRangeAt(0);if (!article.contains(range.commonAncestorContainer)) return; quotePreview.textContent = selection.toString();

Line 2 stops early when there is no Range, so getRangeAt(0) on line 3 can't throw. Line 4 stops when the selection is outside the article. After those two checks, line 6 puts the selected text into a preview box.

Copy the selected text with a helperPop out in the code editor (opens in a new tab)JavaScript
function selectedText() {  const selection = window.getSelection();  return selection?.toString().trim() ?? "";} const range = document.createRange();range.selectNodeContents(article);const selection = window.getSelection();selection.removeAllRanges();selection.addRange(range);console.log(selectedText());

Lines 1 to 4 make a helper function, selectedText(). On line 3:

  • ?. stops and gives undefined if there is no Selection.
  • .trim() removes spaces at the start and the end of the text.
  • ?? swaps undefined for an empty string.

So the helper never throws. A Copy button can turn itself off when the text is empty. Lines 6 to 10 select the whole article. Line 11 prints the article's text.

Reading text does not copy it

selection.toString() only reads the text. To put it on the clipboard, call navigator.clipboard.writeText(text) when the user clicks. That call can fail if the browser or the user says no, so handle the error. A quote preview still works without the clipboard.

Highlight one wordPop out in the code editor (opens in a new tab)JavaScript
const paragraph = document.querySelector("#highlight-demo");const range = document.createRange();range.setStart(paragraph.firstChild, 0);range.setEnd(paragraph.firstChild, 9);const mark = document.createElement("mark");range.surroundContents(mark);console.log(paragraph.innerHTML);

Line 3 puts the start at offset 0 of the text Highlight me. Line 4 puts the end at offset 9, right after Highlight. The space and me are left out. Line 5 makes a <mark>. The Range stays inside one text node, so surroundContents() on line 6 works. Line 7 prints <mark>Highlight</mark> me.

Range, Selection, or text box tools?
  • Copy the nodes between two points on the page, without changing the page
  • Show the text the user just dragged over in an article
  • Put an emoji in at the cursor inside a <textarea>
  • Know whether a drag started at the end and moved left
  • Wrap selected page text in <mark>
  • Select the characters from index 2 up to, but not including, index 5 in a text field
Try it yourself
0 of 6 correct

Sort each task by the tool you would use first.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Fun fact: highlights without a mark element

Newer browsers have the CSS Custom Highlight API. Through CSS.highlights, it colors a Range without adding <mark> elements. Check that CSS.highlights exists before you use it, and have a backup plan. The Range builder above does this.

Common misconceptions

The three tools have similar names. Read each row of the table as: "this tool holds this, so use it for that."

Three tools with similar names
ToolWhat it holdsWhat to use it for
RangeA start point and an end point in the page.Copy, delete, take out, insert, wrap, or read one part of the page.
SelectionWhat the user selected on the page right now, or where the cursor is.Read the selected text, the anchor and the focus, and the first Range.
Input selectionTwo numbers inside the text of a text box.Move the cursor in a text box, select letters, or put in new text.

"A Range offset always counts characters."

Only inside a text node, and even there it counts code units, not the symbols you see. Inside an element, the offset counts child nodes. That is why, with the root element as the container in the Range builder, the offset jumps from the first text node, to the <b>, to the last text node.

"The anchor is always the Range's start."

A Range always keeps its start before its end. The anchor is where the user's drag started. After a backwards drag, the anchor is at the end of the selected words.

"I can use getSelection() for text boxes."

Use selectionStart and selectionEnd for inputs and textareas. They are simpler, and they are made for the text inside the box.

"surroundContents() always wraps whatever is selected."

It throws an error when the Range cuts through an element, such as half of a <b>. A good highlighter catches the error. Then it uses extractContents() and insertNode() instead.

"cloneRange() copies the selected text."

cloneRange() only copies the start point and the end point. To copy the selected nodes, use cloneContents().

Practice: select, read, and change

5 EXERCISES
Exercise 1 · Warm-upCount the emoji's code units

What does this program print? Write both values.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const word = "A😀B";
console.log(word.length);
console.log(word.slice(1, 3));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePick the right tool

    You're making an emoji button for a comment <textarea>. Which property tells you where the cursor starts?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeAct out extractContents() with a string

      This program does with a string what extractContents() does with a Range. What is left in text after line 3?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      let text = "Range magic works";
      const selected = text.slice(6, 11);
      text = text.slice(0, 6) + text.slice(11);
      console.log(selected);
      console.log(text);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeName the method that can fail

        A user selects from plain text into only part of a bold word. Which Range method can throw an error then?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeWatch a backwards selection

          In the live selection inspector, select one word from left to right. Then select the same word from right to left. Write down which values changed and which stayed the same.

            Quiz: check your understanding

            7 QUESTIONS

            Answer each question before you open the explanation. Each wrong answer also explains why it is wrong.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is the start point or end point of a Range made of?

              Choose an answer to see the explanation.

            2. Question 2 of 7What length does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const text = "A😀B";
              console.log(text.length);

              Choose an answer to see the explanation.

            3. Question 3 of 7What should you use to find the cursor inside a textarea?

              Choose an answer to see the explanation.

            4. Question 4 of 7A user drags from the end of a sentence back to its start. Which pair remembers that direction?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which word does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const text = "Range magic works";
              console.log(text.slice(6, 11));

              Choose an answer to see the explanation.

            6. Question 6 of 7When does range.surroundContents(mark) throw?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which sentence about the page's Selection is true?

              Choose an answer to see the explanation.

            Key takeaways

            • A Range has two points. Each point is a container node and an offset.
            • In a text node, the offset counts UTF-16 code units. In an element, it counts child nodes.
            • The Selection is what the user selected right now. Its anchor and focus show which way the user dragged.
            • Inputs and textareas keep their own cursor, as selectionStart and selectionEnd.
            • Before you change selected text, check that it is inside your part of the page. Catch errors from surroundContents().

            Selection & Range in one line.
            A Range marks exactly where; the Selection is what the user selected right now; a text box keeps its own cursor numbers.

            Up next: Introduction to events. You'll learn how your code can react when the user clicks, presses keys, or types.

            CompleteFrontend Clear concepts. Working examples.