In this post, we would create a simple React component which would allow a user to highlight selected text using a mouse. Also, it would also allow an optional callback function, which will receive the selection details.

Research on existing solutions

It’s always a good idea to search for existing well-tested components which may meet our requirements. After some quick search, all similar existing solutions seem to fall into the following categories:

They accept text to be searched as props thus acting more like search/replace utilities, not allowing dynamic selection using mouse. They are part of bigger and complicated component libraries thus needlessly increasing dependencies.

We don’t want both. So let’s create our own solution.

Ironing out the requirements

Our component will support the following props (inputs to the component):

text Text to be shown to the user

Text to be shown to the user selectionHandler (optional) A callback function. It will receive an object containing key details about selection

(optional) A callback function. It will receive an object containing key details about selection customClass (optional) A user-provided CSS class to style the selected text

Component Code

HighLigther Component

Component Logic

Our challenge here is to get the text which got selected when the user selects a certain portion with the mouse. We use DOM’s window.getSelectionAPI for this. This API is called in the mouseUp event. It returns a Selection object with many useful attributes and methods for our selection. The ones which are useful to us are:

anchorNode Node in which the selection begins.

Node in which the selection begins. anchorOffset A number representing the offset of the selection’s anchor within the anchorNode.

A number representing the offset of the selection’s anchor within the anchorNode. focusNode The Node in which the selection ends.

The Node in which the selection ends. focusOffset A number representing the offset of the selection’s anchor within the focusNode.

A number representing the offset of the selection’s anchor within the focusNode. toString() The selected text.

The anchor node and anchor offset give us the distance of start of the selection in terms of character. The end of the selection can be determined by adding the length of selection itself (given by toString method on the Selection object). These start and end point are then used to split our original text into three separate spans identified by unique data-attributes:

first text preceding the selection

text preceding the selection middle selection text (style applied to this span to highlight it)

selection text (style applied to this span to highlight it) last text following selection

All these key info including endpoints, offsets, nodes, and text portions are kept in the components state. If an optional callback is provided, we call it with an object argument containing selection text and endpoints.

Although quite simpler on the surface, there are following hidden complexities in this selection logic:

After the initial selection, new span nodes get introduced (to mark the highlighted text using CSS styling). These changes introduce new anchorNode/focusNode for each span along with relative offsets. For any further selections, we may then have to do relative adjustments depending on which span we started from using the earlier selection endpoints. [line 56–74] Users might do the selection in reverse order. We then have to do our calculations with response to focusNode/focusOffset (shown in previous snippets). We use DOM’s compareDocumentPosition API (which gives us relative position nodes) along with the difference in the offsets to determine the direction of selection. [line 48–52]

Note

There are few alternate ways to do the highlight, including Range.surroundContents API. However, these directly modify the DOM which is discouraged in React. We want to have as limited direct interaction with DOM as possible.

You can find the complete source code here, NPM package here and test it live here.