165

Can someone help me with a javascript function that can highlight text on a web page. And the requirement is to - highlight only once, not like highlight all occurrences of the text as we do in case of search.

5
  • 6
    If you post the code of the function we would be able help. If you ask us to create such a function for you... that's less likely. You have to do something on your own. Start doing something and come back when you get stuck. Commented Dec 27, 2011 at 12:08
  • 7
    YEs I have read How to Ask & I have done something on my own but I got stuck and that's why I asked. I work on Android and have little knowledge of javasript that is why I am not able to do it on my own. Earlier I was using a different javascript which did the job but not without certain limitations. I might not have used the right words while asking this question and I am sorry for that but please do not think of otherwise. Commented Dec 27, 2011 at 12:40
  • 1
    This plugin may be of interest for you: github.com/julmot/jmHighlight . It can highlight keywords separately or as a term, can highlight the match with your custom element and classname and can also search for diacritics. On top it allows you to filter the context in which to search for matches. Commented Sep 24, 2015 at 10:43
  • 1
    Checkout following regex way... stackoverflow.com/a/45519242/2792959 Commented Aug 5, 2017 at 7:16
  • I prepared an article on that here, exhesham.com/2017/11/20/… Commented Dec 4, 2017 at 9:06

28 Answers 28

162

You can use the jquery highlight effect.

But if you are interested in raw javascript code, take a look at what I got Simply copy paste into an HTML, open the file and click "highlight" - this should highlight the word "fox". Performance wise I think this would do for small text and a single repetition (like you specified)

function highlight(text) {
  var inputText = document.getElementById("inputText");
  var innerHTML = inputText.innerHTML;
  var index = innerHTML.indexOf(text);
  if (index >= 0) { 
   innerHTML = innerHTML.substring(0,index) + "<span class='highlight'>" + innerHTML.substring(index,index+text.length) + "</span>" + innerHTML.substring(index + text.length);
   inputText.innerHTML = innerHTML;
  }
}
.highlight {
  background-color: yellow;
}
<button onclick="highlight('fox')">Highlight</button>

<div id="inputText">
  The fox went over the fence
</div>

Edits:

Using replace

I see this answer gained some popularity, I thought I might add on it. You can also easily use replace

"the fox jumped over the fence".replace(/fox/,"<span>fox</span>");

Or for multiple occurrences (not relevant for the question, but was asked in comments) you simply add global on the replace regular expression.

"the fox jumped over the other fox".replace(/fox/g,"<span>fox</span>");

Replacing the HTML to the entire web-page

to replace the HTML for an entire web-page, you should refer to innerHTML of the document's body.

document.body.innerHTML

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks a lot for your reply but can you also tell me how to specify the color in javascript itself
You can replace the "<span class='highlight'>" with "<span style='color: " + color + ";'>", color should be something like var color = "#ff0000";
what if i want to highlight all occurrences of a word on the whole page ?@guy mograbi
Using a simple "replace" is a bad idea. I've described why here: stackoverflow.com/a/32758672/3894981
This isn't a great idea because this will attempt to highlight HTML tags/attributes/etc. For example, what would happen in the case of: <img src="fox.jpg" /> You would get invalid HTML that would look like: <img src="<span class='highlight'>fox</span>.jpg" /> Not good
|
79

The solutions offered here are quite bad.

  1. You can't use regex, because that way, you search/highlight in the html tags.
  2. You can't use regex, because it doesn't work properly with UTF* (anything with non-latin/English characters).
  3. You can't just do an innerHTML.replace, because this doesn't work when the characters have a special HTML notation, e.g. &amp; for &, &lt; for <, &gt; for >, &auml; for ä, &ouml; for ö &uuml; for ü &szlig; for ß, etc.

What you need to do:

Loop through the HTML document, find all text nodes, get the textContent, get the position of the highlight-text with indexOf (with an optional toLowerCase if it should be case-insensitive), append everything before indexof as textNode, append the matched Text with a highlight span, and repeat for the rest of the textnode (the highlight string might occur multiple times in the textContent string).

Here is the code for this:

var InstantSearch = {

    "highlight": function (container, highlightText)
    {
        var internalHighlighter = function (options)
        {

            var id = {
                container: "container",
                tokens: "tokens",
                all: "all",
                token: "token",
                className: "className",
                sensitiveSearch: "sensitiveSearch"
            },
            tokens = options[id.tokens],
            allClassName = options[id.all][id.className],
            allSensitiveSearch = options[id.all][id.sensitiveSearch];


            function checkAndReplace(node, tokenArr, classNameAll, sensitiveSearchAll)
            {
                var nodeVal = node.nodeValue, parentNode = node.parentNode,
                    i, j, curToken, myToken, myClassName, mySensitiveSearch,
                    finalClassName, finalSensitiveSearch,
                    foundIndex, begin, matched, end,
                    textNode, span, isFirst;

                for (i = 0, j = tokenArr.length; i < j; i++)
                {
                    curToken = tokenArr[i];
                    myToken = curToken[id.token];
                    myClassName = curToken[id.className];
                    mySensitiveSearch = curToken[id.sensitiveSearch];

                    finalClassName = (classNameAll ? myClassName + " " + classNameAll : myClassName);

                    finalSensitiveSearch = (typeof sensitiveSearchAll !== "undefined" ? sensitiveSearchAll : mySensitiveSearch);

                    isFirst = true;
                    while (true)
                    {
                        if (finalSensitiveSearch)
                            foundIndex = nodeVal.indexOf(myToken);
                        else
                            foundIndex = nodeVal.toLowerCase().indexOf(myToken.toLowerCase());

                        if (foundIndex < 0)
                        {
                            if (isFirst)
                                break;

                            if (nodeVal)
                            {
                                textNode = document.createTextNode(nodeVal);
                                parentNode.insertBefore(textNode, node);
                            } // End if (nodeVal)

                            parentNode.removeChild(node);
                            break;
                        } // End if (foundIndex < 0)

                        isFirst = false;


                        begin = nodeVal.substring(0, foundIndex);
                        matched = nodeVal.substring(foundIndex, foundIndex + myToken.length);

                        if (begin)
                        {
                            textNode = document.createTextNode(begin);
                            parentNode.insertBefore(textNode, node);
                        } // End if (begin)

                        span = document.createElement("span");
                        span.className += finalClassName;
                        span.appendChild(document.createTextNode(matched));
                        parentNode.insertBefore(span, node);

                        nodeVal = nodeVal.substring(foundIndex + myToken.length);
                    } // Whend

                } // Next i 
            }; // End Function checkAndReplace 

            function iterator(p)
            {
                if (p === null) return;

                var children = Array.prototype.slice.call(p.childNodes), i, cur;

                if (children.length)
                {
                    for (i = 0; i < children.length; i++)
                    {
                        cur = children[i];
                        if (cur.nodeType === 3)
                        {
                            checkAndReplace(cur, tokens, allClassName, allSensitiveSearch);
                        }
                        else if (cur.nodeType === 1)
                        {
                            iterator(cur);
                        }
                    }
                }
            }; // End Function iterator

            iterator(options[id.container]);
        } // End Function highlighter
        ;


        internalHighlighter(
            {
                container: container
                , all:
                    {
                        className: "highlighter"
                    }
                , tokens: [
                    {
                        token: highlightText
                        , className: "highlight"
                        , sensitiveSearch: false
                    }
                ]
            }
        ); // End Call internalHighlighter 

    } // End Function highlight

};

Then you can use it like this:

function TestTextHighlighting(highlightText)
{
    var container = document.getElementById("testDocument");
    InstantSearch.highlight(container, highlightText);
}

Here's an example HTML document

<!DOCTYPE html>
<html>
    <head>
        <title>Example of Text Highlight</title>
        <style type="text/css" media="screen">
            .highlight{ background: #D3E18A;}
            .light{ background-color: yellow;}
        </style>
    </head>
    <body>
        <div id="testDocument">
            This is a test
            <span> This is another test</span>
            äöüÄÖÜäöüÄÖÜ
            <span>Test123&auml;&ouml;&uuml;&Auml;&Ouml;&Uuml;</span>
        </div>
    </body>
</html>

By the way, if you search in a database with LIKE,
e.g. WHERE textField LIKE CONCAT('%', @query, '%') [which you shouldn't do, you should use fulltext-search or Lucene], then you can escape every character with \ and add an SQL-escape-statement, that way you'll find special characters that are LIKE-expressions.

e.g.

WHERE textField LIKE CONCAT('%', @query, '%') ESCAPE '\'

and the value of @query is not '%completed%' but '%\c\o\m\p\l\e\t\e\d%'

(tested, works with SQL-Server and PostgreSQL, and every other RDBMS system that supports ESCAPE)


A revised typescript-version:

namespace SearchTools 
{


    export interface IToken
    {
        token: string;
        className: string;
        sensitiveSearch: boolean;
    }


    export class InstantSearch 
    {

        protected m_container: Node;
        protected m_defaultClassName: string;
        protected m_defaultCaseSensitivity: boolean;
        protected m_highlightTokens: IToken[];


        constructor(container: Node, tokens: IToken[], defaultClassName?: string, defaultCaseSensitivity?: boolean)
        {
            this.iterator = this.iterator.bind(this);
            this.checkAndReplace = this.checkAndReplace.bind(this);
            this.highlight = this.highlight.bind(this);
            this.highlightNode = this.highlightNode.bind(this);    

            this.m_container = container;
            this.m_defaultClassName = defaultClassName || "highlight";
            this.m_defaultCaseSensitivity = defaultCaseSensitivity || false;
            this.m_highlightTokens = tokens || [{
                token: "test",
                className: this.m_defaultClassName,
                sensitiveSearch: this.m_defaultCaseSensitivity
            }];
        }


        protected checkAndReplace(node: Node)
        {
            let nodeVal: string = node.nodeValue;
            let parentNode: Node = node.parentNode;
            let textNode: Text = null;

            for (let i = 0, j = this.m_highlightTokens.length; i < j; i++)
            {
                let curToken: IToken = this.m_highlightTokens[i];
                let textToHighlight: string = curToken.token;
                let highlightClassName: string = curToken.className || this.m_defaultClassName;
                let caseSensitive: boolean = curToken.sensitiveSearch || this.m_defaultCaseSensitivity;

                let isFirst: boolean = true;
                while (true)
                {
                    let foundIndex: number = caseSensitive ?
                        nodeVal.indexOf(textToHighlight)
                        : nodeVal.toLowerCase().indexOf(textToHighlight.toLowerCase());

                    if (foundIndex < 0)
                    {
                        if (isFirst)
                            break;

                        if (nodeVal)
                        {
                            textNode = document.createTextNode(nodeVal);
                            parentNode.insertBefore(textNode, node);
                        } // End if (nodeVal)

                        parentNode.removeChild(node);
                        break;
                    } // End if (foundIndex < 0)

                    isFirst = false;


                    let begin: string = nodeVal.substring(0, foundIndex);
                    let matched: string = nodeVal.substr(foundIndex, textToHighlight.length);

                    if (begin)
                    {
                        textNode = document.createTextNode(begin);
                        parentNode.insertBefore(textNode, node);
                    } // End if (begin)

                    let span: HTMLSpanElement = document.createElement("span");

                    if (!span.classList.contains(highlightClassName))
                        span.classList.add(highlightClassName);

                    span.appendChild(document.createTextNode(matched));
                    parentNode.insertBefore(span, node);

                    nodeVal = nodeVal.substring(foundIndex + textToHighlight.length);
                } // Whend

            } // Next i 

        } // End Sub checkAndReplace 


        protected iterator(p: Node)
        {
            if (p == null)
                return;

            let children: Node[] = Array.prototype.slice.call(p.childNodes);

            if (children.length)
            {
                for (let i = 0; i < children.length; i++)
                {
                    let cur: Node = children[i];

                    // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
                    if (cur.nodeType === Node.TEXT_NODE) 
                    {
                        this.checkAndReplace(cur);
                    }
                    else if (cur.nodeType === Node.ELEMENT_NODE) 
                    {
                        this.iterator(cur);
                    }
                } // Next i 

            } // End if (children.length) 

        } // End Sub iterator


        public highlightNode(n:Node)
        {
            this.iterator(n);
        } // End Sub highlight 


        public highlight()
        {
            this.iterator(this.m_container);
        } // End Sub highlight 


    } // End Class InstantSearch 


} // End Namespace SearchTools 

Usage:

let searchText = document.getElementById("txtSearchText");
let searchContainer = document.body; // document.getElementById("someTable");
let highlighter = new SearchTools.InstantSearch(searchContainer, [
    {
        token: "this is the text to highlight" // searchText.value,
        className: "highlight", // this is the individual highlight class
        sensitiveSearch: false
    }
]);


// highlighter.highlight(); // this would highlight in the entire table
// foreach tr - for each td2 
highlighter.highlightNode(td2); // this highlights in the second column of table

15 Comments

Great answer.. The method looks like overkill, but concise! Will definitely be interested in doing a speed test with that method as in my case the results are lazy loaded into the DOM (as there CAN be thousands of results), curious if this method would add a high latency to the lazy load.
Sorry, but none of your arguments are true. 1. You absolutely can use a RegExp, you just should not search inside the HTML value but the text value of an element. 2. You can absolutely use RegExp with diacritic characters, as implemented in mark.js. 3. HTML notations will be converted to the actual characters in the browser DOM, so you also absolutely use them!
@julmot; To 1: Which means you need to iterate through every element, which is precisely what I do. Unless you don't care about loosing formatting, in which case you can search in document.body.innerText, which will be quite slow. 3. Not in the DOM, but in the innerText or the textContent property of a text-element. Which again means you need to iterate through the text elements; can't be done with regEx AFAIK. 2: Don't know mark.js, but I would avoid everything that does a jQuery.each, because that is damn slow.
@StefanSteiger 1. Then you should correct your decision relationale, as it says that we can't search with a RegExp at all, which isn't true 2. It doesn't use jQuery.each. What makes you think that? 3. This isn't true, at least in Firefox. &auml; e.g. will be converted to the actual character, even when using innerHTML.
Hi @StefanSteiger Actually, I am using your solutions. This one is perfect . But there is some problem like,If I I have a P In which there are two spans and one span is has data like Diploma MSBTE and second span has data 2012 . Now If string which I want to highlight is Diploma MSBTE 2012 , this whole string then I checked this does not work, If everything which is going to matched is present in one span then it works, but if the text content is in diff tags then It does not work. Can you please tell something about this ?
|
71

Why using a selfmade highlighting function is a bad idea

The reason why it's probably a bad idea to start building your own highlighting function from scratch is because you will certainly run into issues that others have already solved. Challenges:

  • You would need to remove text nodes with HTML elements to highlight your matches without destroying DOM events and triggering DOM regeneration over and over again (which would be the case with e.g. innerHTML)
  • If you want to remove highlighted elements you would have to remove HTML elements with their content and also have to combine the splitted text-nodes for further searches. This is necessary because every highlighter plugin searches inside text nodes for matches and if your keywords will be splitted into several text nodes they will not being found.
  • You would also need to build tests to make sure your plugin works in situations which you have not thought about. And I'm talking about cross-browser tests!

Sounds complicated? If you want some features like ignoring some elements from highlighting, diacritics mapping, synonyms mapping, search inside iframes, separated word search, etc. this becomes more and more complicated.

Use an existing plugin

When using an existing, well implemented plugin, you don't have to worry about above named things. The article 10 jQuery text highlighter plugins on Sitepoint compares popular highlighter plugins.

Have a look at mark.js

mark.js is such a plugin that is written in pure JavaScript, but is also available as jQuery plugin. It was developed to offer more opportunities than the other plugins with options to:

  • search for keywords separately instead of the complete term
  • map diacritics (For example if "justo" should also match "justò")
  • ignore matches inside custom elements
  • use custom highlighting element
  • use custom highlighting class
  • map custom synonyms
  • search also inside iframes
  • receive not found terms

DEMO

Alternatively you can see this fiddle.

Usage example:

// Highlight "keyword" in the specified context
$(".context").mark("keyword");

// Highlight the custom regular expression in the specified context
$(".context").markRegExp(/Lorem/gmi);

It's free and developed open-source on GitHub (project reference).

Disclosure: I am the original author of this library.

10 Comments

Highlighting text alone isn't a good enough reason for me to include jQuery.
@Roy I've taken this to heart. Good news, as of v6.0.0 mark.js waived the jQuery dependency and makes it now optionally to use it as jQuery plugin.
All true, except: 1st point is not possible, because you cannot get registered event handlers, and even if you could, you couldn't set anonymous functions... 2nd: mark.js does not find text between two tags either, e.g. <span>s</span>ed won't find sed... 3rd: whenever a browser (including new version) comes along that you haven't tested it yet, it might break. That is always true, no matter how many tests you write. At 17kb, marks is too big for what it does.
Someone else's library is still "selfmade."
You should disclose in the answer, that you are the author of this library explicitly.
|
15

None of the other solutions really fit my needs, and although Stefan Steiger's solution worked as I expected I found it a bit too verbose.

Following is my attempt:

/**
 * Highlight keywords inside a DOM element
 * @param {string} elem Element to search for keywords in
 * @param {string[]} keywords Keywords to highlight
 * @param {boolean} caseSensitive Differenciate between capital and lowercase letters
 * @param {string} cls Class to apply to the highlighted keyword
 */
function highlight(elem, keywords, caseSensitive = false, cls = 'highlight') {
  const flags = caseSensitive ? 'g' : 'gi';
  // Sort longer matches first to avoid
  // highlighting keywords within keywords.
  keywords.sort((a, b) => b.length - a.length);
  Array.from(elem.childNodes).forEach(child => {
    const keywordRegex = RegExp(keywords.join('|'), flags);
    if (child.nodeType !== 3) { // not a text node
      highlight(child, keywords, caseSensitive, cls);
    } else if (keywordRegex.test(child.textContent)) {
      const frag = document.createDocumentFragment();
      let lastIdx = 0;
      child.textContent.replace(keywordRegex, (match, idx) => {
        const part = document.createTextNode(child.textContent.slice(lastIdx, idx));
        const highlighted = document.createElement('span');
        highlighted.textContent = match;
        highlighted.classList.add(cls);
        frag.appendChild(part);
        frag.appendChild(highlighted);
        lastIdx = idx + match.length;
      });
      const end = document.createTextNode(child.textContent.slice(lastIdx));
      frag.appendChild(end);
      child.parentNode.replaceChild(frag, child);
    }
  });
}

// Highlight all keywords found in the page
highlight(document.body, ['lorem', 'amet', 'autem']);
.highlight {
  background: lightpink;
}
<p>Hello world lorem ipsum dolor sit amet, consectetur adipisicing elit. Est vel accusantium totam, ipsum delectus et dignissimos mollitia!</p>
<p>
  Lorem ipsum dolor sit amet, consectetur adipisicing elit. Numquam, corporis.
  <small>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium autem voluptas perferendis dolores ducimus velit error voluptatem, qui rerum modi?</small>
</p>

I would also recommend using something like escape-string-regexp if your keywords can have special characters that would need to be escaped in regexes:

const keywordRegex = RegExp(keywords.map(escapeRegexp).join('|')), flags);

3 Comments

This worked well for me but it also needs a way to "unmark"'
Works well. Thanks. @param {boolean} caseSensitive is the opposite
@jwzumwalt Look at my answer.
12
function stylizeHighlightedString() {

    var text = window.getSelection();

    // For diagnostics
    var start = text.anchorOffset;
    var end = text.focusOffset - text.anchorOffset;

    range = window.getSelection().getRangeAt(0);

    var selectionContents = range.extractContents();
    var span = document.createElement("span");

    span.appendChild(selectionContents);

    span.style.backgroundColor = "yellow";
    span.style.color = "black";

    range.insertNode(span);
}

5 Comments

Mohit, welcome to SO. Some description of the code would be nice!
shouldnt' there be a way to select text without creating another node?
@user191433 the question is not just about selecting text, but also applying styles. For that you need a node.
Reminder/tip that the JavaScript span.style.backgroundColor = "yellow"; translates to CSS style="background-color: yellow;" --that subtle difference between the camelCase and dashed-notation tripped me up at first.
P.S. Mohit's answer at stackoverflow.com/questions/7991474/… is a more-streamlined variant of this code. (for instance omitting the start and end variables which are solely diagnostic/non-functional here.)
11

Here's my regexp pure JavaScript solution:

function highlight(text) {
    document.body.innerHTML = document.body.innerHTML.replace(
        new RegExp(text + '(?!([^<]+)?<)', 'gi'),
        '<b style="background-color:#ff0;font-size:100%">$&</b>'
    );
}

5 Comments

This works perfectly for me when the block of text I am trying to highlight contains HTML tags.
You can also tweak the function to accept multiple words via the regexp pipe symbol, e.g. one|two|three
It will not replace the text if the end of the text has a > character. Modify the regex using (?!([^<]+)?<) for it to work.
Modified as requested.
Perfect! This is the best for me
9

Since HTML5 you can use the <mark></mark> tags to highlight text. You can use javascript to wrap some text/keyword between these tags. Here is a little example of how to mark and unmark text.

JSFIDDLE DEMO

4 Comments

innerHTML is dangerous. It will delete events.
This also doesn't work properly because, for example, if you enter into the JSFIDDLE "Lorem", it only marks the first instance of it.
Wel you just need to replace all occurrences of the keyword. here is an example with regex globally jsfiddle.net/de5q704L/73
This is not a decent solution, mark is basically just a div with a yellow background...
8

Fast forward to 2019, Web API now has native support for highlighting texts:

const selection = document.getSelection();
selection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);

And you are good to go! anchorNode is the selection starting node, focusNode is the selection ending node. And, if they are text nodes, offset is the index of the starting and ending character in the respective nodes. Here is the documentation

They even have a live demo

2 Comments

oh this is brilliant. simply use it like this: selection.setBaseAndExtent(desiredNode, 0, desiredNode, 1); to highlight the only node you need. and it works with Gutenberg
Nice, but still marked mostly as experimental.
8

If you also want it to be highlighted on page load, there is a new way.

just add #:~:text=Highlight%20These

try accessing this link in a new tab

https://stackoverflow.com/questions/38588721#:~:text=Highlight%20a%20text

which highlights the "Highlight a text" text.

Also, currently only supported on Chrome (Thanks GitGitBoom).

3 Comments

For anyone looking for more information: this is called Scroll to Text Fragment and is currently only supported on Chrome. chromestatus.com/feature/4733392803332096
1) doesn't work in all browsers. 2) only works on page's first load. 3) only highlights the first match. 4) no way to edit the css styles. good finding though.
4

I have the same problem, a bunch of text comes in through a xmlhttp request. This text is html formatted. I need to highlight every occurrence.

str='<img src="brown fox.jpg" title="The brown fox" />'
    +'<p>some text containing fox.</p>'

The problem is that I don't need to highlight text in tags. For example I need to highlight fox:

Now I can replace it with:

var word="fox";
word="(\\b"+ 
    word.replace(/([{}()[\]\\.?*+^$|=!:~-])/g, "\\$1")
        + "\\b)";
var r = new RegExp(word,"igm");
str.replace(r,"<span class='hl'>$1</span>")

To answer your question: you can leave out the g in regexp options and only first occurrence will be replaced but this is still the one in the img src property and destroys the image tag:

<img src="brown <span class='hl'>fox</span>.jpg" title="The brown <span 
class='hl'>fox</span> />

This is the way I solved it but was wondering if there is a better way, something I've missed in regular expressions:

str='<img src="brown fox.jpg" title="The brown fox" />'
    +'<p>some text containing fox.</p>'
var word="fox";
word="(\\b"+ 
    word.replace(/([{}()[\]\\.?*+^$|=!:~-])/g, "\\$1")
    + "\\b)";
var r = new RegExp(word,"igm");
str.replace(/(>[^<]+<)/igm,function(a){
    return a.replace(r,"<span class='hl'>$1</span>");
});

2 Comments

This was the only regex solution that worked for me without messing with <img src="word"> or <a href="word">.
Golden rule: Never. Use. Regular. Expressions. To. Mess. About. With. XML.
4

CSS Custom Highlight API

Browser vendors are working towards explicit support for the highlighting pattern - with an incredibly versatile new "Custom Highlight" API. This will be the definitive solution to the common highlighting problem.

Make sure that your whole audience is using a supported browser.

The API has been available in Chrome derivatives since version 105 (2022-09-02), and is currently supported in Firefox Nightly.

https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API

Implementation

The MDN article above contains implementation details, so the only real question is your DOM highlighting rules, and their algorithm. Are you searching only inside elements (aka. on (groups of) DOM text nodes), or should matching carry over element boundaries? If so, do you want to perform a RegExp match on the concatenated text nodes of some root element, or should certain elements (such as block-level elements like div) mark boundaries in the text?

  • You'll probably need an algorithm1 that steps through the tree under your root element to gather "text flows" (which could be one "text flow").
  • Pair this with a matching algorithm which, for each of your RegExps, matches the RegExp over the concatenation of the DOM text nodes and then walks through the results, storing the details of the match.

An example of this would be rather long. The first algorithm is not too hard, but the second one is very fiddly, so I'll link to a 35 line example which is part of a larger highlighting implementation2:

https://github.com/searchmarkers/mark-my-search/{COMMIT}/src/modules/highlight/matcher.mts#L30

Parameters:

  • terms is an array of objects ("MatchTerm"), each of which identifies a highlighting request and contains a RegExp.
  • text is the concatenation of the DOM text nodes in the next parameter.
  • textFlow is an array of DOM text nodes (considered to "flow" into each other).

Specific variables:

  • boxesInfo is an array of objects ("BoxInfo"), each of which describes a highlight box ready to be applied to the document:
    • term is the MatchTerm associated with the highlight box, so that e.g. the correct highlighting colour can be applied.
    • node is the DOM text node which the highlight box is contained inside. If a match spans multiple nodes, it is described using multiple BoxInfo's.
    • start is the start position of the highlight box within the node.
    • end is the end position of the highlight box within the node.

1 Consider using a TreeWalker object for this.

2 I maintain a browser extension called Mark My Search; this code is a component used by its 3 highlighting engines. One uses the Highlight API, and one uses a choice of APIs including the Painting API, while the last uses the classic approach of splitting text nodes to insert elements.

Mark My Search is licensed under the MIT License, meaning you can take and modify the code as you like.

2 Comments

This is the best solution to use in 2025. It is supported on all browsers except Firefox, and it has the advantage of not modifying the DOM like the other solutions, which can cause a lot of headaches.
I have written a very lightweight package which does it, vanilla-js or React - github.com/yairEO/react-css-highlight/blob/master/src/vanilla/…
3

Simple TypeScript example

NOTE: While I agree with @Stefan in many things, I only needed a simple match highlighting:

module myApp.Search {
    'use strict';

    export class Utils {
        private static regexFlags = 'gi';
        private static wrapper = 'mark';

        private static wrap(match: string): string {
            return '<' + Utils.wrapper + '>' + match + '</' + Utils.wrapper + '>';
        }

        static highlightSearchTerm(term: string, searchResult: string): string {
            let regex = new RegExp(term, Utils.regexFlags);

            return searchResult.replace(regex, match => Utils.wrap(match));
        }
    }
}

And then constructing the actual result:

module myApp.Search {
    'use strict';

    export class SearchResult {
        id: string;
        title: string;

        constructor(result, term?: string) {
            this.id = result.id;
            this.title = term ? Utils.highlightSearchTerm(term, result.title) : result.title;
        }
    }
}

Comments

3

Simply pass your word into the following function:

function highlight_words(word) {
    const page = document.body.innerHTML;
    document.body.innerHTML = page.replace(new RegExp(word, "gi"), (match) => `<mark>${match}</mark>`);
}

Usage:

highlight_words("hello")

This will highlight all instances of the word on the page.

Comments

2

I think this code is better because highlight all repeated character

function highlight(text) {
  var inputText = document.getElementById("inputText");
  var innerHTML = inputText.innerHTML;
  var index = innerHTML.indexOf(text);
  if (index >= 0) { 
     inputText.innerHTML=innerHTML.split(text).join('<span class="highlight">'+text+'</span>');
  }
}
.highlight {
  background-color: yellow;
}
<button onclick="highlight('fox')">Highlight</button>

<div id="inputText">
  The fox went over the fence fox fox fox wen fox
</div>

1 Comment

OP said that they do not want to highlight all occurrences.
1

I was wondering that too, you could try what I learned on this post.

I used:

function highlightSelection() {
			var userSelection = window.getSelection();
			for(var i = 0; i < userSelection.rangeCount; i++) {
				highlightRange(userSelection.getRangeAt(i));
			}
			
		}
			
			function highlightRange(range) {
			    var newNode = document.createElement("span");
			    newNode.setAttribute(
			       "style",
			       "background-color: yellow; display: inline;"
			    );
			    range.surroundContents(newNode);
			}
<html>
	<body contextmenu="mymenu">

		<menu type="context" id="mymenu">
			<menuitem label="Highlight Yellow" onclick="highlightSelection()" icon="/images/comment_icon.gif"></menuitem>
		</menu>
		<p>this is text, select and right click to high light me! if you can`t see the option, please use this<button onclick="highlightSelection()">button </button><p>

you could also try it here: http://henriquedonati.com/projects/Extension/extension.html

xc

Comments

1

If you surround any text inside of the mark tag, it will automatically get highlighted by the browser in this striking yellow color. Details are available here: https://dev.to/comscience/highlight-searched-text-on-a-page-with-just-javascript-17b3

 <h1>
  Searching and Marking
</h1>

<input type="text" id="search"/>
<button onClick="search(id)" id="button">
Highlight
</button>

<p id="text">
What exactly is this Worker thread module, and why do we need it? In this post, we will talk about the historical reasons concurrency is implemented in JavaScript and Node.js, the problems we might find, current solutions, and the future of parallel processing with Worker threads.

Living in a single-threaded world
JavaScript was conceived as a single-threaded programming language that ran in a browser. Being single-threaded means that only one set of instructions is executed at any time in the same process (the browser, in this case, or just the current tab in modern browsers).

This made things easier for implementation and for developers using the language. JavaScript was initially a language only useful for adding some interaction to webpages, form validations, and so on — nothing that required the complexity of multithreading.
</p>

Now JS code will look like this

function search(e) {
    let searched = document.getElementById("search").value.trim();
  if (searched !== "") {
    let text = document.getElementById("text").innerHTML;
    let re = new RegExp(searched,"g"); // search for all instances
        let newText = text.replace(re, `<mark>${searched}</mark>`);
        document.getElementById("text").innerHTML = newText;
  }
}

Comments

1

On cybernetic: Thanks, the function below works. But there's a problem because it replaces also the words inside tags. Below is an example if the word to highlight is target:

<a <mark>target</mark>="_blank" href="Its not OK to highlight <mark>target</mark> here">Its OK to highlight the words <mark>target</mark>s here</a>

How do we prevent this?

function highlight_words(word) {
    const page = document.body.innerHTML;
    document.body.innerHTML = page.replace(new RegExp(word, "gi"), (match) => `<mark>${match}</mark>`);
}

Comments

1

I implemented something similar recently. My goal was to implement text highlight upon search. Here is how I did it -

Created a regex with the received input.

const regex = new RegExp(`(${value})`, 'gi');

Traversed through the element tree of the concerning element. And then searched for the received input in the text nodes (instead of the innerHTML to avoid matches in the attributes eg. class, href. etc.). When a match was received. It (text node) was swapped with a wrapper span containing the required span. For eg.

text ---> <wrapper-span><span> text </span></wrapper-span>

So, I was able to get my text highlighted.

highlightStr(value, regex, document.getELementById('searchable-div'));

const highlightStr = (value, regex, node) => {
        node.childNodes.forEach((childNode) => {
            if (childNode.nodeValue && regex.test(childNode.nodeValue)) {
                const highLightWrapper = document.createElement('span');
                highLightWrapper.classList.add("highlight-wrapper-class");

                childNode.replaceWith(highLightedWrapper);

                highLightWrapper.innerHTML = childNode.nodeValue.replace(regex, `<span class="highlight">${value}</span>`);
            }
            this.highlightStr(value, regex, childNode);
        });
    }

But now the problem is if we do not remove (or check for existing) wrapper-span structure before trying to highlight a text again. We might end with an infinite wrapper span tree or a broken search. So I implemented a reset function for it. You might want to run it every time you would like to highlight the text. It basically just swaps back the text node with its text content. So we get exactly what we had before highlighting the text.

resetHighlight(document.getELementById('searchable-div'));

const resetHighlight = (node) => {
        node.childNodes.forEach((childNode) => {
            if ((childNode as HTMLElement).classList && (childNode as HTMLElement).classList.contains('highlight-wrapper-class') && childNode.textContent) {
                childNode.replaceWith(childNode.textContent);
            }
            this.resetHighlight(childNode);
        });
    }

Comments

1

use mark.js

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/7.0.0/mark.min.js"></script>

....(although there may be other versions beyond 7.0 by now)

With a very simple implementation :

<script>
    function doHighlightText(findtx) {
        if (findtx.length > 1) { // only bother to highlight if the text is more than 1 char long
            var instance = new Mark(document.querySelector('#container'));
            instance.unmark({
                done: function () {
                    instance.mark(findtx, {
                        caseSensitive: false,
                        separateWordSearch: false
                    });
                }
            });
        }
        else {
            var instance = new Mark(document.querySelector('#container'));
            instance.unmark();
        }
        return;
    }
</script>

See https://jsfiddle.net/Abeeee/9guLs35w/ for a running example.

Comments

0

Using the surroundContents() method on the Range type. Its only argument is an element which will wrap that Range.

function styleSelected() {
  bg = document.createElement("span");
  bg.style.backgroundColor = "yellow";
  window.getSelection().getRangeAt(0).surroundContents(bg);
}

Comments

0

Here's something I got working with React.js

import React from 'react';
interface HighlightProps {
  text: string;
  searchTerm: string;
  highlightStyle?: React.CSSProperties;
}
const defaultHighlightStyle: React.CSSProperties = {
  backgroundColor: 'yellow',
};
const Highlight: React.FC<HighlightProps> = ({
  text,
  searchTerm,
  highlightStyle = defaultHighlightStyle,
}) => {
  if (!searchTerm) {
    return <span>{text}</span>;
  }
  const regex = new RegExp(`(${searchTerm})`, 'gi');
  const parts = text.split(regex);
  return (
    <span>
      {parts.map((part, index) =>
        part.toLowerCase() === searchTerm.toLowerCase() ? (
          <span key={index} style={highlightStyle}>
            {part}
          </span>
        ) : (
          <React.Fragment key={index}>{part}</React.Fragment>
        ),
      )}
    </span>
  );
};

export default Highlight;

Call it like this:

const ExampleComponent = () => {
    const someContent = 'the quick brown fox jumped over the lazy dog';
    const textToHighlight = 'fox';
    return (
        <Highlight text={someContent} searchTerm={textToHighlight} />
    );
}

Comments

0

just simpledd:

const id = 'bc_data_kode_barang'
const txt = document.getElementById(id).innerText;
document.getElementById(id).innerHTML = `<mark>${txt}</mark>`

work in 2024

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
0

I would like to share more about the usage of the scroll text fragment

syntax: #:~:text=[prefix-,]textStart[,textEnd][,-suffix]

If you want to highlight multiple text fragments in one URL (&text=)

Example Demo link
#:~:text=javascript&text=highlight&text=Ankit How to highlight text using javascript

Comments

0

After lot's of investigation this was the only solution worked for me!

function highlightTextAcrossNodes(container, phraseToHighlight) {
  const walker = document.createTreeWalker(
    container,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );

  const textNodes = [];
  let fullText = '';
  let node;

  while ((node = walker.nextNode())) {
    textNodes.push({
      node,
      start: fullText.length,
      end: fullText.length + node.textContent.length
    });
    fullText += node.textContent;
  }

  const matchIndex = fullText.indexOf(phraseToHighlight);
  if (matchIndex === -1) {
    console.warn('Text not found');
    return false;
  }

  const matchEnd = matchIndex + phraseToHighlight.length;

  let range = document.createRange();
  let startFound = false;

  for (const { node, start, end } of textNodes) {
    if (!startFound && matchIndex >= start && matchIndex < end) {
      range.setStart(node, matchIndex - start);
      startFound = true;
    }
    if (startFound && matchEnd <= end) {
      range.setEnd(node, matchEnd - start);
      break;
    }
  }

  if (!range || range.collapsed) {
    console.warn('Failed to build a proper range');
    return false;
  }

  const mark = document.createElement('mark');
  try {
    const contents = range.extractContents();
    mark.appendChild(contents);
    range.insertNode(mark);
    return true;
  } catch (err) {
    console.error('Error during highlightTextAcrossNodes highlighting:', err);
    return false;
  }
}

Comments

-1

Too late to the party, but here are my 2 two cents:

Hightlight text:

Mostly the version of user @elclanrs, but with some tweaks: "CaseSensitive" means it is sensitive to the case, so the case does matter.

                     /**
                     * @param {string} elem Element to search for keywords in
                     * @param {string[]} keywords Keywords to highlight
                     * @param {boolean} caseInSensitive True for ignoring the case of the letters
                     * @param {string} cssClass Class to apply to the highlighted keyword
                     */
                    function highlightInsideElement(elem, keywords, caseInSensitive = true, cssClass) {
                        const flags = caseInSensitive ? 'gi' : 'g';
                        // Sort longer matches first to avoid
                        // highlighting keywords within keywords.
                        keywords.sort((a, b) => b.length - a.length);
                        Array.from(elem.childNodes).forEach(child => {
                            const keywordRegex = RegExp(keywords.join('|'), flags);
                            if (child.nodeType !== 3) { // not a text node
                                highlightInsideElement(child, keywords, caseInSensitive, cssClass);
                            } else if (keywordRegex.test(child.textContent)) {
                                const frag = document.createDocumentFragment();
                                let lastIdx = 0;
                                child.textContent.replace(keywordRegex, (match, idx) => {
                                    const part = document.createTextNode(child.textContent.slice(lastIdx, idx));
                                    const highlighted = document.createElement('span');
                                    highlighted.textContent = match;
                                    highlighted.classList.add(cssClass);
                                    highlighted.style.background='yellow';
                                    frag.appendChild(part);
                                    frag.appendChild(highlighted);
                                    lastIdx = idx + match.length;
                                });
                                const end = document.createTextNode(child.textContent.slice(lastIdx));
                                frag.appendChild(end);
                                child.parentNode.replaceChild(frag, child);
                            }
                        });
                    }
                                           
                    highlightInsideElement(document.body, ['hello'], true, 'highlighted');

Remove highlightings:

                    /**
                     * @param {string} nodeToRemoveHighlightingsIn Node to remove highlights from.
                     * @param {string} cssClass Class used for highlighting.
                     */
                    function removeHighlightings(nodeToRemoveHighlightingsIn, cssClass) {
                      Array.from(nodeToRemoveHighlightingsIn.querySelectorAll(`.${cssClass}`)).forEach(span => {
                        const parent = span.parentNode;
                        parent.replaceChild(document.createTextNode(span.textContent), span);
                        parent.normalize();
                      });
                    }
                                    
                    removeHighlightings(document.body, 'highlighted');

Comments

-1

Do not invent the wheel, and use an existing library for that purpose. Like https://markjs.io/

Comments

-1

function highlight(text) {
  var inputText = document.getElementById("inputText");
  var innerHTML = inputText.innerHTML;
  var index = innerHTML.indexOf(text);
  if (index >= 0) { 
   innerHTML = innerHTML.substring(0,index) + "<span class='highlight'>" + innerHTML.substring(index,index+text.length) + "</span>" + innerHTML.substring(index + text.length);
   inputText.innerHTML = innerHTML;
  }
}
.highlight {
  background-color: yellow;
}
<button onclick="highlight('fox')">Highlight</button>

<div id="inputText">
  The fox went over the fence
</div>

1 Comment

there are already lots of answers here, with high votes. Adding a code only answer doesn't serve the reades here, and it doesn't serve your answer by maybe getting less appreciation than it deserves. Edit your answer and add some lines of explanation to fix this....
-1

enter image description here

This uses the browser built-in selection to mark text. Thus the html elements are not modified. Firefox even supports multiple selections.

function highlight(query) {
  var sel = window.getSelection()

  sel.collapse(foo, 0)
  if (query == "") return

  // find all query locations
  ranges = []
  while (window.find(query)) ranges.push(sel.getRangeAt(0))

  for (range of ranges) window.getSelection().addRange(range)
}
<input onchange="highlight(this.value); this.selectionStart=1">

<input type="button" value="find">

<div id="foo">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</div>

original code (using Range)

function highlight(query) {
  window.getSelection().removeAllRanges()
  if (query == "") return

  // find all query locations
  idx = []
  ii = -1
  while ((ii = foo.innerHTML.indexOf(query, ii + 1)) > -1) idx.push(ii)

  for (i of idx) {
    range = new Range();
    range.setStart(foo.childNodes[0], i);
    range.setEnd(foo.childNodes[0], i + query.length);
    window.getSelection().addRange(range)
  }
}
<input oninput="highlight(this.value)">
<input type="button" value="find">

<div id="foo">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</div>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.