This Gatsby post was written for Gatsby 3 and is no longer maintained. If you would like to try out SvelteKit or Astro, check out the maintained posts which are based on those. Astro lets you continue to use React but also with partial hydration which can be used to provide an enhanced user experience. SvelteKit offers server side rendering as well as static site generation. See the post on how performance improved switching from Gatsby to Astro for more background.
We look at how to highlight search results using Web APIs. Although the example uses Gatsby, the React code can be used on Next.js and other popular React based frameworks. This post follows on from the recent post where we used fast JS Search to generate site search results. When we talk about site search, we mean the user searching for content on your own site, from your own site. In this post, we improve on the user experience, by highlighting search terms in the summaries displayed for results. On top, when the user clicks one of the results, we highlight the search term (as well as related terms) on the actual result. One more thing… We scroll the first search match into view for the ultimate user experience.
We will do most of that using semantic HTML and standard browser APIs as much as possible. So to
highlight code we will use the HTML
<mark> text element and for scrolling, Element.scrollIntoView(), from the browser Web API .
First, we will improve the search results we generated last time by adding highlights. After that, we add search to the result itself. Finally, we will scroll the first match into view automatically when the user opens a search result.
If you want to follow along, it might be easier to start with the earlier post. Just fire up your dev server by following instructions in that post. No problem, though, if you are just looking to see what you can lift for your Next.js project.
First, we will create a utility function for finding occurrences of a search term and stem
equivalents in a block of text. By stem equivalents, we mean if the user searches for
“films” as well as highlight that word, we also highlight the words
“film”, “filmed”, “filming” and so on. For this we will use
the same stemmer, we used in the earlier post. Create a file at
src/utilities/search.js and add the following content:
Here we convert the block of text into an array of words, using a regular expression, via a utility function in line 3. This makes the subsequent search easier. We convert all words to lower case, so we can compare apples with apples, so to speak.
You will probably remember, last time we rendered search results in the
BlogPostSummary component. We want to highlight search results in that component. For that to work, we need to
pass the search term from the
Search component to the
BlogPostSumary component. Update the
It is important that we convert the search term to lower case, as we won’t check it is lower case in the following code.
Next, we update the
BlogPostSummary component to take the new
searchTerm prop we are passing in:
We have also imported our new utility function and added some extra imports which we will use later.
Just like we had to pass the search term from the
BlogPostSummary component, we need to pass it on to the
actual post we will render if the user clicks the summary. This time we will use a different
mechanism, though. We will pass the search term as a search parameter in the page URL. This makes
more sense here, as the post is not a child component of the
BlogPostSummary component (
is rendered by the
Search component). Anyway, we
need to change the link we navigate to. Let's edit the
BlogPostSummary component once more:
Note, we now need this
useEffect block (lines
38) to run when
searchTerm changes, so we include that variable in the array in line
postLink function (line
tacks the search term on the end of the slug as a URL search parameter. We have added the
similarWords state variable, which we will use next:
Here we are using the new utility function to tell us which words in the summary we will need to
highlight. We put those words into an array state variable,
similarWords. Naturally, when the search term changes (as the user types in the search box in the Search
component), we need to update this array. For that reason,
searchTerm is added in line
We have set up all the background work needed to highlight the search term (and stem equivalents) in the component. Now let's add the code which does the actual highlighting:
Let's see what we have done here. In line 46, we split the text at each occurrence of an element
similarWords array. This generates an array of shorter
strings, which (combined) reproduce the original input text. The block in lines
57 looks at each of these parts in turn.
If the part is a
similarWords element, we wrap it in a
<mark> element. By default, the browser will highlight this text without us having to do anything
else. The rest of the loop just glues everything else back together as it was. We have to add keys
to the elements. There is a nice Twitter thread by React guru Dan Abramov on Why React needs keys . We use
uuid to generate unique keys. Install this package to
get the code to work:
Finally, there are some cases where there will not be any stem equivalents generated (this might
happen for very short search terms), even though, we want to highlight the search term. Lines
68 handle this case.
Next, in line
72 we define a function that uses the new code above
to highlight the meta description. We simply need to call this function to highlight the rendered
There are a couple more updates in here which should make sense without an explanation. That's the first part, and in fact most of the work complete. Next, let’s look at how we highlight the search term in the blog post.
Now we switch to the blog post template component. We want this component to look exactly the same as it did before, when there is no search term. We pass the search term in as a search parameter in the post URL. When a search parameter is included, we will highlight it wherever it occurs in the text, as well as stem equivalents. We need to run a new search for stem equivalents as we do not have access to the previous results, besides, there may be extra stem equivalents which occur in the full post, but not in the meta description.
Let's start by adding new imports:
Next we want to pull the search term from the browser URL, we did something similar in the
As well as getting the search term, we added a
variable, just like in the
BlogPostSumary component. On top, we
update this array when the search term updates. We are not expecting the user to change the search
term in this component, but the search term may not be available on initial render so once it is
updated, we need to update the
### Highlight Search Term and Stem Equivalents
Next, we want to highlight the search term in our blog post. This is a little more complicated than before, as our blog post is in MDX, and we use MDXRenderer to render it. Luckily, MDXRenderer accepts a wrapper. We will use that wrapper to highlight the code. Let’s have a look at how this is done:
Starting at the bottom, we add the wrapper to the
whenever we have a search term (line
69). We were already using
shortcodes in the rendered content, so we do not need to
change anything with the rendered content we return for our component. The manipulation of the DOM
elements using React APIs, in the
highlightWords function, is a
little more involved. To keep focus on the task at hand, we will not go into this. You can learn more about what we are doing here in an excellent post on React children on Max Stoibers
Now, if you search for a term which is in the text and open up the search result, our code will
highlight it. Essentially, this just comes from adding
<mark> elements. We are almost done now. The last step is to scroll the first occurrence of a
highlighted term into view. This is quite handy for users in a hurry, they won't need to scan
through a potentially long post to find a match and work out if our post is what they were after.
We have done all the hard work already. All we need to do now is add one final
useEffect block to assist with scroll into view. This will bring the first match to the top of the
Here we are using the standard web API. The code just finds the first highlight element (note, we
added this class to highlight terms in the previous step (line
Try going to the home page, clicking the search icon. Search for “optical”. The word “optics” should be highlighted in the second result returned. Although there is a match within the first post's text, the work does not appear in the rendered content in the summary, so there is nothing to highlight. Click the second result and “optics” should be highlighted and at the top of the browser window. What do you think?
You can see the completed code in the Rodney Lab GitHub page . There is also a live demo site at gatsby-site-search.rodneylab.com .
In this post, we learned:
- how to use semantic HTML to highlight text;
- some ways to improve user experience when returning search results; and
how to use the
scrollIntoViewWeb API in React.
I hope you found this useful. There are more extensions you can add. For example, when the user searches for a term which is in the text but not in the meta description, you could render the passage of text which contains the search term in the summary. Currently, we render the meta description, which doesn't give the user much context in this case. On top, you could add a drop-down list of search terms to the search box. You can then add term selection using up and down arrow keys. Colby Fayock has an excellent video on doing this in Next.js . Finally, you can style the mark elements to highlight text with a different colour to the browser default.
Keen to hear how you plan to use this! Is there something from this post you can leverage for a side project or even client project? I do hope so! Let me know if there is anything in the post that I can improve on, for anyone else creating this project. You can leave a comment below, @ me on Twitter or try one of the other contact methods listed below.
If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Gatsby JS among other topics. Also, subscribe to the newsletter to keep up-to-date with our latest projects.