Click, drag, release: you’ve just selected some text on a webpage — probably to copy and paste it somewhere or to share it. Wouldn’t it be cool if selecting that text revealed some options that make those tasks easier? That’s what a selection menu does.
You may already be familiar with selection menus if you’ve ever used an online editor. When you select text, options to format the selection might float above it. In fact, I’m writing this draft in an editor that does exactly this.
Let’s see how we can create a selection menu like this using JavaScript’s Selection API. The API gives us access to the space and content of the selected area on a webpage. This way we can place the selection menu exactly above the selected text and get access to the selected text itself.
Here’s an HTML snippet with some sample text:
<article>
<h1>Select over the text below</h1>
<p>Cascading Style Sheets (CSS) is a style sheet language used for describing the presentation of a document written in a markup language such as HTML. CSS is a cornerstone technology of the World Wide Web, alongside HTML and JavaScript. CSS is designed to enable the separation of presentation and content, including layout, colors, and fonts. This separation can improve content accessibility, provide more flexibility and control in the specification of presentation characteristics. </p>
</article>
<template><span id="control"></span></template>
There’s a <template>
tag at the end there. The <span>
inside it is our selection menu control. Anything inside a <template>
tag is not rendered on the page until it’s later added to the page with JavaScript. We’ll add the selection menu control to the page when user selects text. And when the user selects that text, our selection menu will prompt the user to tweet it.
Here’s the CSS to style it:
#control {
background-image: url("data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width='40px' height='40px'><foreignObject width='40px' height='40px'><div xmlns='http://www.w3.org/1999/xhtml' style='width:40px;height:40px;line-height:40px;text-align:center;color:transparent;text-shadow: 0 0 yellow, 2px 4px black, -1px -1px black;font-size:35px;'>💬</div></foreignObject></svg>");
cursor: pointer;
position: absolute;
width: 40px;
height: 40px;
}
#control::before{
background-color: black;
color: white;
content: " tweet this! ";
display: block;
font-weight: bold;
margin-left: 37px;
margin-top: 6px;
padding: 2px;
width: max-content;
height: 20px;
}
Check out this article to learn how I used an emoji (💬) for the background image.
So far, the sample text is ready, and the selection menu control has been styled. Let’s move on to the JavaScript. When a selection is made, we’ll get the size and position of the selected area on the page. We then use those measurements to assign the position of the selection menu control at the top-middle of the selected area.
var control = document.importNode(document.querySelector('template').content, true).childNodes[0];
document.querySelector('p').onpointerup = () => {
let selection = document.getSelection(), text = selection.toString();
if (text !== "") {
let rect = selection.getRangeAt(0).getBoundingClientRect();
control.style.top = `calc(${rect.top}px - 48px)`;
control.style.left = `calc(${rect.left}px + calc(${rect.width}px / 2) - 40px)`;
control['text']= text;
document.body.appendChild(control);
}
}
In this code, we first get a copy of the selection menu control inside <template>
, then assign it to the control
variable.
Next, we write the handler function for the onpointerup
event of the element carrying the sample text. Inside the function, we get the selection and the selected string using document.getSelection()
. If the selected string is not empty, then we get the selected area’s size and position, via getBoundingClientRect()
, and place it in the rect
variable.
Using rect
, we calculate and assign the top
and left
positions of the control
. This way, the selection menu control is placed a little above the selected area and centered horizontally. We’re also assigning the selected string to a user-defined property of control
. This will later be used to share the text.
And, finally, we add control
to the webpage using appendChild()
. At this point, if we select some of the sample text on the page, the selection menu control will appear on the screen.
Now we get to code what happens when the selection menu control is clicked. In other words, we’re going to make it so that the text is tweeted when the prompt is clicked.
control.addEventListener('pointerdown', oncontroldown, true);
function oncontroldown(event) {
window.open(`https://twitter.com/intent/tweet?text=${this.text}`)
this.remove();
document.getSelection().removeAllRanges();
event.stopPropagation();
}
When the control is clicked, a tab opens with Twitter’s “New Tweet” page, complete with the selected text ready to go.
After the tweet prompt, the selection menu control is no longer needed and is removed, along with any selection made on the page. The way that the pointerdown
event cascades further down the DOM tree is also stopped at this point.
We also need an event handler for the onpointerdown
event of the page:
document.onpointerdown = ()=> {
let control = document.querySelector('#control');
if (control !== null) {control.remove();document.getSelection().removeAllRanges();}
}
Now the control and any selection made on the page are removed when clicking anywhere on the page but the selection menu control.
Demo
Here’s a more prettified version that Chris put together:
And here’s an example showing more than one control in the selection menu:
About that <template>
It’s not totally necessary that we use it. Instead, you can also try simply hiding and showing the control some other way, like the hidden
HTML attribute or the CSS display
. You can also build a selection menu control in the JavaScript itself. The coding choices will depend on how efficiently you execute them, and their fallbacks, if needed, as well as how they fit in with your application.
Some UI/UX advice
While this is a nice effect, there are a couple of things to consider when using it to ensure a good user experience. For example, avoid injecting your own text into the text selection — you know, like appending a link back to your site in the auto-generated tweet. It’s intrusive and annoying. If there’s any reason to do that, like adding the source citation, let the user see a preview of the final text before posting it. Otherwise, the user might be confused or surprised by the addition.
One more thing: It’s best if the menu control is out of the way. We don’t want it covering up too much of the surrounding content. That sort of thing adds up to CSS “data loss” and we want to avoid that.
Bottom line: Understand why your users need to select text on your website and add the controls in a way that gets out of the way of what they’re trying to do.
How does this interact with using the browser’s
right-click menu to copy/print/otherwise interact with the selection?
In you examples above, right-clicking on the selected text deselects the text making it impossible to interact with.
If you use this do you have to duplicate the functions of the right-click menu like copy to clipboard?
Ctrl+C and Ctrl+V do work, but I would guess you need to reimplement copying using the mouse. ‘Enhancements’ like these inherently assume that you need to prevent the browser from doing anything useful and then add your own code.
No need. The only thing you need to do is check which mouse button was pressed in the event handler and do nothing if it was a right click.
What about smartphones? How to disable the native context menu?
Hey friends, just a headsup, the proposed solution using
pointerdown
seems to mess with selecting text on ios. :)Your JS code is not working. I keep getting the same error: Uncaught TypeError: Cannot set property ‘top’ of undefined. That includes the following line: control.style.top =
calc(${rect.top}px - 48px)
;In CodePen, though, it works just fine.
How to fix it?