What Text Area Popovers Taught Me About Browser APIs
Developer Advocate @ GitHub
Search for a command to run...
Developer Advocate @ GitHub
I really enjoyed reading this article, I also learned about the Selection API. Safe delivery Rizel Scarlett 🙏
In this series, I will create technical tutorials for navigating the Open Source ecosystem as a beginner!
When you think of open source contributions, do you immediately think of code? It’s a common misconception that you have to code to contribute to open source. Open source is for everyone. Folks with various skill sets, like – designers, writers, art...
Raise your hand if you've been personally victimized by the question: 'Will AI replace software engineers?' It's a common debate that drives developers to extremes—either avoiding AI entirely or frantically signing up for every AI course available. ...
This blog post uses storytelling to introduce beginners to Verifiable Credentials, followed by a tutorial on creating them in JavaScript and Kotlin. In Our Last Issue... In our previous issue, we met Dawson Webhart, a junior frontend software enginee...

"Our current credit system says, 'Saddle yourself, so we can give you more to saddle.'" Eric Lapin, President of FormFree, stated during a recent TBD live stream. His comment sheds light on a fundamental paradox of the U.S. credit system: it often re...

Technology adoption doesn't happen with one software engineer coding in solitude. Even though big companies like Meta and Microsoft boast of their humble beginnings in a dorm room or a garage, product adoption requires good marketing. Many software e...

Black Girl Bytes
58 posts
I'm a Developer Advocate at GitHub. I'm passionate about community, code, and education! Reach out to me on Twitter ❤️
I recently went down several rabbit holes about building WYSIWYG editors and popovers. While on maternity leave, I finally have time to deeply explore random technical problems without deadlines - a rare opportunity.
Thus far, I've focused on contributing to an open source AI agent called codename goose built with a Rust backend and Electron-based chat interface. I submitted a pull request to add a WYSIWYG editor to its chat interface, opting to build a custom solution instead of using existing packages. The maintainers appreciated this approach since editor dependencies bloat bundle size, but raised concerns about the toolbar consuming too much visual space. They suggested implementing a popover toolbar instead.
I wrongly assumed creating a floating toolbar would be simple. I aimed to:
Show a popover toolbar when text is selected
Position it precisely above the selection
Handle word-wrapped text spanning multiple lines
Maintain accurate positioning during scroll
Handling popovers in a text area element turned out to be more complex than expected. In this blog post, I’ll share what I learned.
Unlike typical HTML elements where we can manipulate contents, measure positions, and add new elements, text areas only expose raw text content and basic selection APIs. Browsers control their rendering behind the scenes.
I asked Claude to generate an analogy for further illustration:
Regular HTML elements are like having a house where you can move the furniture around, add new items, measure distances between things, etc.
Text areas are more like looking through a window into a room you can't enter. You can see what's inside and make some basic changes (like adding/removing text), but you can't reach in and manipulate things directly. The browser handles all the internal workings using native OS text editing capabilities.
All modern browsers include a built-in Popover API for creating popup elements. Here's an example:
While this API is cross-browser compatible and straightforward to implement, it comes with several limitations:
It only works with button elements since the required popovertarget attribute is only available on buttons
You have to use CSS to position the popover relative to its target element
And, my biggest limitation is that it doesn’t work within text areas.
Shout out to Mark Techson for introducing me to the Popover API via Una Kravets’ conference talk called Less Cruft, More Power: Leverage the Power of the Web Platform.
I wanted my popover to appear wherever the user selected text. This required me to:
Know the position of the selected text
Listen for events that happened when text was selected and deselected
I came across Colby Fayock’s blog post called How to Share Selected Text in React with the Selection API. While Colby's focus was on text sharing functionality, his post introduced me to the Selection API - which could help me position my popover relative to selected text.
The Selection API lives in window.getSelection(). When you call this method, it returns a Selection object that tells you about the text a user has selected on the page.
From this Selection object, you can call getRangeAt(0) to find out exactly where the selection begins and ends. It gives you two numbers:
startOffset - where the selection begins
endOffset - where the selection ends
In a selection, each character has an index. For the text "Hello, World! Welcome.", the indexes look like this:
If you select the word "World", then startOffset is 7 (where "W" begins) and endOffset is 12 (right after "d").
Side note: I learned that the parameter 0 in getRangeAt(0) tells the browser which selection you want information about. When you select text, each selection gets stored at different indexes in an array. Most browsers only let you select one piece of text at a time, so you'll only have one item at index 0. But browsers like Firefox let you hold Ctrl to select multiple pieces of text. If you try to access indexes greater than 0 in browsers that don't support multiple selections, you'll get an error.
getRangeAt(0) gives you access to getBoundingClientRect().
getBoundingClientRect() returns a box of measurements around your selected text. It tells you the top, right, bottom, and left positions of this box, plus its width and height.
With these measurements, I could place my popover right above any text the user selects, like this:
While this approach worked for most HTML elements, text area elements provide limited access to the Selection API, so I needed an alternative approach.
Through rubber ducking with Claude, I learned about the mirrored div approach, a workaround for determining selection coordinates in a text area.
Here's how it works: create an invisible div that overlays the text area, containing the exact same content and styling. When a user selects text, they're actually interacting with this invisible div rather than the text area underneath it. This gives you access to the full Selection API while maintaining what looks and feels like a standard text area to the user.
I found validation for this technique in Jhey Thompkins' blog post "HOW TO: Where's the text cursor?", which introduced me to the getComputedStyle() method. This API returns the computed CSS styles of HTML elements, letting developers match the text area's appearance in the overlay div with precision.
But just like with a real mirror, things aren't always what they seem. Similar to the warning on car mirrors that "objects may be closer than they appear," our mirrored div can distort reality in subtle ways:
Text can wrap at different points between the div and text area
Browsers handle spacing and font rendering differently, causing text positions to shift unexpectedly
{% codesandbox wtsqcv %}
I’ve tried a few packages, and I found that most of these packages work well with regular DOM elements. However, they struggle with text areas due to the same fundamental limitations I stated earlier - limited access to the text area's internal rendering and positioning.
While browsers have come a long way in supporting rich text interactions, working with text areas remains surprisingly complex. I had fun learning about these browser APIs, but I still haven't found a solution that fits my use case. Maybe future APIs will make tasks like selection-based popovers more straightforward.
If you've tackled text area customization in a way I haven’t explored, I'd love to hear about your approaches.