How to Show a Floating Toolbar on Text Selection

Nitish Kumar Singh

May 31, 2025

Text selection is a simple user action, but detecting it gracefully isn't as easy. Showing a floating toolbar on selected text requires an understanding of browser events.

In this post, we'll explore how text selection works, the challenges with touch support, and how to build a responsive floating toolbar that centers on selected text and works smoothly across devices.

Before we start, let’s look at the different ways we can select text on a webpage. Knowing these will help us understand how to detect text selection better. Here are the main ways:

  • Mouse: We can select text either by double-click or click-and-drag using a mouse.
  • Touch: Long press or touch-and-drag using a finger on a touchscreen.
  • Keyboard: Using Ctrl, Shift, and arrow keys, or selecting all by Ctrl+A.

What’s our goal here? We want to show a floating toolbar on selected text that tries to center itself after the selection is completed—not while the user is still selecting.

We can show it after text selection is completed when selection is done by mouse and keyboard, but we can’t when it’s done using a touchscreen. Here, I’ll show it after selection only for mouse, and during selection in case of touch and keyboard.

This is because there’s no event that fires exactly when text gets selected by touch-and-drag, as the browser handles text selection on touch screens differently.

To detect when text selection is completed, we use mousedown and mouseup events. And the selectionchange event comes into play for keyboard and touch selections.

So below is the code for a React component that does all this and shows the floating toolbar.

import { useEffect, useRef, useState } from "react";

export default function FloatingToolbar(){
  const [visible, setVisible] = useState(0);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const isMouseDown = useRef(false);
  const toolbarRef = useRef(null);

  const getSelectionRect = () => {
    const selection = window.getSelection();
    return !selection || selection.toString() === "" ? null : selection.getRangeAt(0).getBoundingClientRect();
  };

  const updateToolbar = ({type}) => {
    const rect = getSelectionRect();    
    if((!isMouseDown.current || type === "mouseup") && rect){
      setVisible(-1);
      requestAnimationFrame(()=>{
        const pRect = toolbarRef.current.parentElement.getBoundingClientRect();
        const w = toolbarRef.current.clientWidth;
        let left = rect.left - pRect.left + rect.width / 2 - w / 2;
        left = Math.max(5, Math.min(left, pRect.width - w - 5)); // 5px away from parent edge
        setPosition({top: rect.bottom - pRect.top, left: left});
        setVisible(1);
      });
    }else setVisible(0);
    isMouseDown.current = type === "mousedown" ? true : type === "mouseup" ? false : isMouseDown.current;    
  };

  useEffect(() => {
    toolbarRef.current.parentElement.addEventListener('mousedown', updateToolbar);
    toolbarRef.current.parentElement.addEventListener('mouseup', updateToolbar);
    toolbarRef.current.parentElement.addEventListener("selectionchange", updateToolbar);

    return () => {
      toolbarRef.current.parentElement.removeEventListener('mouseup', updateToolbar);
      toolbarRef.current.parentElement.removeEventListener('mousedown', updateToolbar);
      toolbarRef.current.parentElement.removeEventListener("selectionchange", updateToolbar);
    };
  }, []);

  return (
    <div contentEditable={false} ref={toolbarRef} className='ftToolbar' style={{display:visible?"block":"none",visibility:visible===1?"visible":"hidden",top:position.top,left:position.left}}>
      <div>
        <button><span className="material-symbols-outlined">title</span></button>
        <button><span style={{fontSize:"21px"}} className="material-symbols-outlined">title</span></button>
        <button><span className="material-symbols-outlined">format_bold</span></button>
        <button><span className="material-symbols-outlined">format_italic</span></button>
        <button><span className="material-symbols-outlined">format_underlined</span></button>
        <button><span className="material-symbols-outlined">link</span></button>
      </div>
      <span className='arrow'/>
    </div>
  );
};

This component perfectly shows the toolbar without letting it go outside the parent’s rect. We're using visible in three states to get the correct width of the toolbar for calculations to keep it inside the view of its parent.

All the CSS for styling the toolbar is below:

.ftToolbar{
  position: absolute;
  user-select: none;
  transform: translate(0,8px);
}
.ftToolbar > div{
  background: #2F2F2F;
  padding: 5px 10px;
  border-radius: 10px;
  display: flex;
  gap: 10px;
  z-index: 5;
}
.ftToolbar button{
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
  color: white;
}
.ftToolbar button span{pointer-events: none;}
.ftToolbar button:hover span{
  scale: 1.2;
  transition: scale 0.1s linear;
}
.arrow {
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 10px solid transparent;
  border-right:10px solid transparent;
  border-bottom: 10px solid #2F2F2F;
  position: absolute;
  left: 50%;
  translate: -50%;
  top: -9px;
}

So this is how I’ve built a floating toolbar that shows on selected text — by detecting different events, handling them properly, calculating positions, and keeping the toolbar centered and within the screen.

I hope you enjoyed building this with me, understood the logic clearly, learned something new about browser selection handling, and found this post helpful.

Thanks for reading! 🤝 Happy Coding! ✨

Published on May 31, 2025
Comments (undefined)

Read More