Frontend Development 21 min read

Master Selections and Cursors in Web Development: Complete Guide with Code Samples

This comprehensive tutorial explains how selections and cursors work in web pages, covering the underlying Selection and Range APIs, handling editable and non‑editable elements, manipulating text in inputs, textareas, and rich content, and providing practical code snippets for every scenario.

Yuewen Frontend Team
Yuewen Frontend Team
Yuewen Frontend Team
Master Selections and Cursors in Web Development: Complete Guide with Code Samples

In web development you often need to work with selections and cursors—for example highlighting text, showing toolbars, or manually moving the caret. A selection is the portion of the document highlighted by the mouse, usually displayed in blue.

The cursor is that blinking vertical line.

Tip: The article is long; reading it completely lets you fully control selections and cursors.

1. What are "selection" and "cursor"?

Conclusion: The cursor is a special kind of selection.

To understand this you need to know two important objects: Selection and Range . Both have many properties and methods; see the MDN docs for details.

Selection represents the user‑selected text range or the current caret position. It may span multiple elements.

Range represents a fragment of the document that contains nodes and parts of text nodes. The selection object gives you a range that is the focus of cursor operations. You can obtain a selection via the global getSelection() method.

<code>const selection = window.getSelection();</code>

Usually you don’t manipulate the selection object directly; instead you work with the range it contains.

<code>const range = selection.getRangeAt(0);</code>

In browsers that support multiple selections (currently only Firefox), selection.getRangeAt() requires an index because there can be several ranges. Most of the time you can ignore this.

You can retrieve the selected text simply with selection.toString() or selection.getRangeAt(0).toString() :

<code>window.getSelection().toString();
// or
window.getSelection().getRangeAt(0).toString();</code>

The Range property collapsed indicates whether the start and end points are the same. When collapsed is true , the selection is reduced to a single point, which appears as the blinking cursor in editable elements.

2. Editable Elements

Editable elements are the only ones that display a cursor. Common editable elements are the default form controls input and textarea , or any element with contenteditable="true" or the CSS -webkit-user-modify property.

<code>&lt;input type="text"&gt;
&lt;textarea&gt;&lt;/textarea&gt;</code>
<code>&lt;div contenteditable="true"&gt;Editable content&lt;/div&gt;</code>
<code>div { -webkit-user-modify: read-write; }</code>

Form controls are easier to control because browsers provide dedicated APIs.

3. Selection Operations on input and textarea

For these elements you usually use setSelectionRange instead of the generic Selection / Range APIs.

inputElement.setSelectionRange(selectionStart, selectionEnd[, selectionDirection]);
<code>btn.onclick = () => {
  txt.setSelectionRange(0, 2);
  txt.focus();
};</code>

To select the entire content you can call select() :

<code>btn.onclick = () => {
  txt.select();
  txt.focus();
};</code>

To move the caret to a specific position you set the start and end to the same offset:

<code>btn.onclick = () => {
  txt.setSelectionRange(2, 2); // place caret after the first two characters
  txt.focus();
};</code>

To restore a previous selection you can store selectionStart and selectionEnd and later call setSelectionRange with those values:

<code>const pos = {};
document.onmouseup = (ev) => {
  pos.start = txt.selectionStart;
  pos.end = txt.selectionEnd;
};
btn.onclick = () => {
  txt.setSelectionRange(pos.start, pos.end);
  txt.focus();
};</code>

To replace or insert text you can use setRangeText :

inputElement.setRangeText(replacement); inputElement.setRangeText(replacement, start, end, [selectMode]);
<code>btn.onclick = () => {
  txt.setRangeText('❤️❤️❤️');
  txt.focus();
};</code>

The optional selectMode determines what happens after replacement (e.g., keep the original selection, place the caret before, after, or select the new text).

4. Selection Operations on Normal Elements

Normal (non‑form) elements don’t have the convenience methods above, so you need to work with the generic Selection and Range APIs.

To create a range you call document.createRange() , set its start and end with range.setStart(node, offset) and range.setEnd(node, offset) , then add it to the selection:

selection.addRange(range);
<code>const selection = document.getSelection();
const range = document.createRange();
range.setStart(txt.firstChild, 0);
range.setEnd(txt.firstChild, 2);
selection.removeAllRanges();
selection.addRange(range);
</code>

When dealing with rich text you can also use range.selectNode(node) or range.selectNodeContents(node) to select an entire element or its contents.

<code>range.selectNode(txt.childNodes[1]); // selects the <span> element containing "阅文"
</code>

To select a specific character inside a nested element you set the start and end on the text node directly:

<code>range.setStart(txt.childNodes[1].firstChild, 0);
range.setEnd(txt.childNodes[1].firstChild, 1);
</code>

For arbitrary offsets in a complex DOM tree you can walk the tree, collect all text nodes, compute their cumulative lengths, and map a global offset to a specific node and inner offset. The article provides a helper function getNodeAndOffset that does this.

<code>function getNodeAndOffset(wrap_dom, start = 0, end = 0) {
  const txtList = [];
  const map = function(children) {
    [...children].forEach(el => {
      if (el.nodeName === '#text') {
        txtList.push(el);
      } else {
        map(el.childNodes);
      }
    });
  };
  map(wrap_dom.childNodes);
  const clips = txtList.reduce((arr, item, index) => {
    const end = item.textContent.length + (arr[index - 1] ? arr[index - 1][2] : 0);
    arr.push([item, end - item.textContent.length, end]);
    return arr;
  }, []);
  const startNode = clips.find(el => start >= el[1] && start < el[2]);
  const endNode = clips.find(el => end >= el[1] && end < el[2]);
  return [startNode[0], start - startNode[1], endNode[0], end - endNode[1]];
}
</code>

Using this helper you can select any range regardless of the element structure:

<code>const nodes = getNodeAndOffset(txt, 7, 12);
range.setStart(nodes[0], nodes[1]);
range.setEnd(nodes[2], nodes[3]);
selection.removeAllRanges();
selection.addRange(range);
</code>

To insert content at the current range you can use range.insertNode(newNode) after optionally calling range.deleteContents() . For example, inserting plain text:

<code>const newNode = document.createTextNode('I am new content');
range.deleteContents();
range.insertNode(newNode);
</code>

Or inserting an element and moving the caret after it:

<code>const mark = document.createElement('mark');
mark.textContent = 'I am new content';
range.deleteContents();
range.insertNode(mark);
range.setStartAfter(mark);
txt.focus();
</code>

To wrap the current selection with a tag you can use range.surroundContents(element) . If the selection is discontinuous (spans multiple nodes), surroundContents throws an error, so a safer approach is to extract the contents, append them to a new element, and insert that element:

<code>const mark = document.createElement('mark');
mark.append(range.extractContents());
range.insertNode(mark);
</code>
These APIs are not exhaustive, but they cover most everyday scenarios. For a complete reference, consult the MDN documentation.

References

Section: https://developer.mozilla.org/en-US/docs/Web/API/Selection Range: https://developer.mozilla.org/en-US/docs/Web/API/Range getSelection: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getSelection setSelectionRange: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange setRangeText: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setRangeText CodePen examples: https://codepen.io/xboxyan/pen/LYOdXpB and https://codepen.io/xboxyan/pen/dyZmQNw

frontendJavaScriptWeb APIDOMCursorselectionrange
Yuewen Frontend Team
Written by

Yuewen Frontend Team

Click follow to learn the latest frontend insights in the cultural content industry. We welcome you to join us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.