Show Tooltip Over Text Selection

November 12, 2021

I'm using React.js in this example.

Let's start with the tooltip component. We'll use CSS and a bit of JS to show and hide the tooltip.

const Tooltip = ({ tooltipRef, showTooltip }) => (
  <div ref={tooltipRef} className={`tooltip ${showTooltip ? 'tooltip--visible' : ''}`}>
    Tooltip content
  </div>
)
.tooltip {
  height: 40px;
  width: 150px;
  padding: 5px;
  background-color: #dcdcdc;
  position: fixed;
  visibility: hidden;
}

.tooltip--visible {
  visibility: visible;
}

Now we'll create the main component with some text.

const App = () => {
  const tooltipRef = React.useRef()
  const [tooltipVisible, toggleTooltip] = React.useState(false)

  return (
    <div style={{ maxWidth: '600px', margin: 'auto' }}>
      <Tooltip tooltipRef={tooltipRef} showTooltip={tooltipVisible} />
      <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam neque urna, tincidunt a
        scelerisque sed, placerat vitae lectus. Praesent velit nibh, egestas vitae laoreet non,
        porttitor id erat. Pellentesque velit tortor, porttitor non magna et, accumsan euismod dui.
        Duis eget nibh in turpis faucibus cursus. Praesent accumsan odio orci, in laoreet ipsum
        porttitor auctor. Integer ligula sapien, varius eget augue ac, ornare dignissim lectus.
        Fusce quis sapien lorem. Sed posuere viverra augue convallis feugiat. Aliquam elit ex,
        dictum eu mauris in, vestibulum interdum augue. Donec ut lorem dapibus, pulvinar arcu at,
        mattis sem. Aenean dictum, lacus vel bibendum lacinia, dui leo eleifend erat, eu eleifend
        quam augue nec dolor
      </p>
      <p>
        Suspendisse dictum lobortis nunc luctus convallis. Proin mauris elit, elementum ut cursus
        eu, luctus condimentum ligula. Nulla malesuada nulla semper pharetra venenatis. Aliquam vel
        lorem pulvinar, auctor erat nec, pretium tellus. Phasellus metus mauris, porta sed convallis
        sit amet, lacinia eget neque. Quisque eu lacus vulputate, ultricies arcu quis, rutrum sem.
        Proin est leo, iaculis in fermentum sit amet, elementum non metus. Cras rhoncus metus eget
        magna bibendum aliquam. Phasellus suscipit turpis vel eros lacinia cursus. Curabitur nec ex
        at risus bibendum maximus vel blandit sapien. Curabitur accumsan posuere urna imperdiet
        vulputate. Quisque mattis diam nec quam aliquet interdum. Suspendisse nec turpis id purus
        dapibus dapibus. Fusce dignissim quis libero congue auctor. Vestibulum vestibulum eget dui
        vel bibendum. Nullam a vehicula neque.
      </p>
    </div>
  )
}

Next, we'll need to add the mouseup event listener.

...

const onMouseUp = () => {
  //
}

useEvent('mouseup', onMouseUp)
...

useEvent hook

import { useEffect } from 'react'

export default function useEvent(event, handler) {
  useEffect(() => {
    window.addEventListener(event, handler)

    return () => {
      window.removeEventListener(event, handler)
    }
  })
}

We'll use the Selection object to determine the position of the tooltip.

...

const onMouseUp = () => {
  const selection = window.getSelection()
  const range = selection.getRangeAt(0)

  toggleTooltip(!tooltipVisible)
  positionTooltip(range)
}

useEvent('mouseup', onMouseUp)

...

postionTooltip function

...

const positionTooltip = range => {
  const rect = range.getClientRects()[0]

  if (rect) {
    const { left, top, width, height } = rect

    const tooltip = tooltipRef.current

    const selectionCenter = left + width / 2
    const tooltipWidth = tooltip.offsetWidth

    let tooltipLeft = selectionCenter - tooltipWidth / 2
    tooltipLeft = tooltipLeft < 1 ? 0 : tooltipLeft

    const tooltipTop = top + height + 10

    tooltip.style.left = `${tooltipLeft}px`
    tooltip.style.top = `${tooltipTop}px`
  }
}

...

That's it.

Full example

import React from 'react'
import useEvent from './useEventHook'

const Tooltip = ({ tooltipRef, showTooltip }) => (
  <div ref={tooltipRef} className={`tooltip ${showTooltip ? 'tooltip--visible' : ''}`}>
    Tooltip content
  </div>
)

const App = () => {
  const tooltipRef = React.useRef()
  const [tooltipVisible, toggleTooltip] = React.useState(false)

  const positionTooltip = range => {
    const rect = range.getClientRects()[0]

    if (rect) {
      const { left, top, width, height } = rect

      const tooltip = tooltipRef.current

      const selectionCenter = left + width / 2
      const tooltipWidth = tooltip.offsetWidth

      let tooltipLeft = selectionCenter - tooltipWidth / 2
      tooltipLeft = tooltipLeft < 1 ? 0 : tooltipLeft

      const tooltipTop = top + height + 10

      tooltip.style.left = `${tooltipLeft}px`
      tooltip.style.top = `${tooltipTop}px`
    }
  }

  const onMouseUp = () => {
    const selection = window.getSelection()
    const range = selection.getRangeAt(0)

    toggleTooltip(!tooltipVisible)
    positionTooltip(range)
  }

  useEvent('mouseup', onMouseUp)

  return (
    <div style={{ maxWidth: '600px', margin: 'auto' }}>
      <Tooltip tooltipRef={tooltipRef} showTooltip={tooltipVisible} />
      <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam neque urna, tincidunt a
        scelerisque sed, placerat vitae lectus. Praesent velit nibh, egestas vitae laoreet non,
        porttitor id erat. Pellentesque velit tortor, porttitor non magna et, accumsan euismod dui.
        Duis eget nibh in turpis faucibus cursus. Praesent accumsan odio orci, in laoreet ipsum
        porttitor auctor. Integer ligula sapien, varius eget augue ac, ornare dignissim lectus.
        Fusce quis sapien lorem. Sed posuere viverra augue convallis feugiat. Aliquam elit ex,
        dictum eu mauris in, vestibulum interdum augue. Donec ut lorem dapibus, pulvinar arcu at,
        mattis sem. Aenean dictum, lacus vel bibendum lacinia, dui leo eleifend erat, eu eleifend
        quam augue nec dolor
      </p>
      <p>
        Suspendisse dictum lobortis nunc luctus convallis. Proin mauris elit, elementum ut cursus
        eu, luctus condimentum ligula. Nulla malesuada nulla semper pharetra venenatis. Aliquam vel
        lorem pulvinar, auctor erat nec, pretium tellus. Phasellus metus mauris, porta sed convallis
        sit amet, lacinia eget neque. Quisque eu lacus vulputate, ultricies arcu quis, rutrum sem.
        Proin est leo, iaculis in fermentum sit amet, elementum non metus. Cras rhoncus metus eget
        magna bibendum aliquam. Phasellus suscipit turpis vel eros lacinia cursus. Curabitur nec ex
        at risus bibendum maximus vel blandit sapien. Curabitur accumsan posuere urna imperdiet
        vulputate. Quisque mattis diam nec quam aliquet interdum. Suspendisse nec turpis id purus
        dapibus dapibus. Fusce dignissim quis libero congue auctor. Vestibulum vestibulum eget dui
        vel bibendum. Nullam a vehicula neque.
      </p>
    </div>
  )
}

export default App