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.
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><input type="text">
<textarea></textarea></code> <code><div contenteditable="true">Editable content</div></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
Yuewen Frontend Team
Click follow to learn the latest frontend insights in the cultural content industry. We welcome you to join us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.