Programming & Development / April 26, 2025

Interactive Text Highlighting in React: Building a Smooth UX

React Highlight Text User Selection Interactive UI React Hooks Frontend UX JavaScript RSX Mouse Events

Highlighting text interactively is a common feature in modern web applications, especially in educational tools, note-taking apps, and content editors. In this article, we will build an intuitive text highlighting feature using React and RSX, where users can select part of a text and highlight it with a small, smooth pop-up action.

Setting Up the Basic Highlight Feature

First, let's create a simple list item (<li>) where users can select any portion of the text. Once selected, the text will be highlighted upon action.

We use React hooks like useState to manage highlights and listen for user selection with the onMouseUp event.

Full Working Code

Here’s the complete React component that allows you to select part of the text and highlight it with a popup button:

tsx

import React, { useState, useRef } from 'react';

const HighlightableListItem: React.FC = () => {
  const [text] = useState('This is a sample text inside the list item. You can select and highlight parts.');
  const [highlights, setHighlights] = useState<{ start: number; end: number }[]>([]);
  const [showButton, setShowButton] = useState(false);
  const [buttonPosition, setButtonPosition] = useState({ top: 0, left: 0 });
  const [selectionRange, setSelectionRange] = useState<{ start: number; end: number } | null>(null);

  const liRef = useRef<HTMLLIElement>(null);

  const handleMouseUp = (e: React.MouseEvent) => {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) {
      setShowButton(false);
      return;
    }

    const range = selection.getRangeAt(0);
    const selectedText = selection.toString();
    const start = range.startOffset;
    const end = range.endOffset;

    if (selectedText.length > 0 && range.startContainer === range.endContainer) {
      // Only allow selection inside the li
      if (liRef.current && liRef.current.contains(range.startContainer)) {
        const rect = range.getBoundingClientRect();
        setButtonPosition({ top: rect.top - 30 + window.scrollY, left: rect.left + window.scrollX });
        setSelectionRange({ start, end });
        setShowButton(true);
      }
    } else {
      setShowButton(false);
    }
  };

  const handleHighlight = () => {
    if (selectionRange) {
      setHighlights([...highlights, selectionRange]);
    }
    setShowButton(false);
    window.getSelection()?.removeAllRanges();
  };

  const renderHighlightedText = () => {
    if (highlights.length === 0) return text;

    let elements = [];
    let lastIndex = 0;

    const sortedHighlights = [...highlights].sort((a, b) => a.start - b.start);

    for (const { start, end } of sortedHighlights) {
      if (lastIndex < start) {
        elements.push(<span key={lastIndex}>{text.slice(lastIndex, start)}</span>);
      }
      elements.push(
        <span key={start} style={{ backgroundColor: 'yellow' }}>
          {text.slice(start, end)}
        </span>
      );
      lastIndex = end;
    }

    if (lastIndex < text.length) {
      elements.push(<span key={lastIndex}>{text.slice(lastIndex)}</span>);
    }

    return elements;
  };

  return (
    <div style={{ position: 'relative', padding: '20px' }}>
      <ul>
        <li
          ref={liRef}
          onMouseUp={handleMouseUp}
          style={{ userSelect: 'text', cursor: 'text', lineHeight: '1.6' }}
        >
          {renderHighlightedText()}
        </li>
      </ul>

      {showButton && (
        <button
          style={{
            position: 'absolute',
            top: buttonPosition.top,
            left: buttonPosition.left,
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '5px',
            padding: '5px 10px',
            fontSize: '12px',
            cursor: 'pointer',
            zIndex: 1000,
          }}
          onClick={handleHighlight}
        >
          Highlight
        </button>
      )}
    </div>
  );
};

export default HighlightableListItem;

How It Works

  1. The text is displayed inside an <li>.
  2. The user selects part of the text.
  3. A floating "Highlight" button appears near the selection.
  4. Clicking the button highlights the selected text in yellow.
  5. The app supports multiple highlights without overwriting previous ones.

Handling Edge Cases

While implementing interactive text highlighting, it’s important to handle:

  • Selections crossing multiple elements (limit it within a single <li>).
  • Empty selections (don't show button if no text selected).
  • Clearing selections after clicking "Highlight".

The above code addresses these edge cases carefully using window.getSelection() and verifying the selection context.

Future Improvements

After setting up basic highlighting, you can further enhance it by:

  • Offering multiple highlight colors (yellow, green, blue, etc.).
  • Adding an undo feature to remove highlights.
  • Enabling commenting on highlighted parts.
  • Saving highlights to a backend database (for persistent storage).

These improvements can make your app feel even more professional and interactive.

Conclusion

Creating a text highlighting feature in React is easier than it sounds.

With React hooks, simple mouse event listeners, and smart conditional rendering, you can build a beautiful and efficient interactive UX.

Adding features like floating highlight buttons massively improves the overall feel and usability of your React application.


Comments

No comments yet

Add a new Comment

NUHMAN.COM

Information Technology website for Programming & Development, Web Design & UX/UI, Startups & Innovation, Gadgets & Consumer Tech, Cloud Computing & Enterprise Tech, Cybersecurity, Artificial Intelligence (AI) & Machine Learning (ML), Gaming Technology, Mobile Development, Tech News & Trends, Open Source & Linux, Data Science & Analytics

Categories

Tags

©{" "} Nuhmans.com . All Rights Reserved. Designed by{" "} HTML Codex