← Back toMirror a text area

Show a toolbar after selecting text in a text area

Written byPhuoc Nguyen
Created
01 Oct, 2023
Including a toolbar that appears after selecting text in a text area can significantly enhance the user experience. Not only does it give users access to commonly used formatting options, but it also saves them time and effort by eliminating the need to navigate menus or remember keyboard shortcuts.
With a toolbar available, users can effortlessly apply formatting such as bold, italic, underline, and font size without interrupting their workflow. They can also quickly add hyperlinks or adjust text alignment with just a click of a button.
By providing users with these convenient options, they can focus on their content instead of worrying about formatting mechanics. This results in a more efficient and enjoyable user experience.
In this post, we will create a simple Markdown editor. The primary editor uses a text area that allows users to modify the contents. Once users select text, a toolbar will appear, providing them with the ability to format the selected text quickly.

Creating the layout

Let's talk about how we can create the layout for our text area. We can use the same approach we used to highlight the current line in the text area. Here's an example of how the layout could look:
html
<div class="container" id="container">
<div class="container__overlay">
<div class="container__toolbar" class="container__toolbar"></div>
<div class="container__mirror"></div>
</div>
<textarea id="textarea" class="container__textarea"></textarea>
</div>
The layout consists of two elements: the mirrored element and the toolbar. The toolbar is positioned over the text area using the `.container__toolbar` class, which has a `position: absolute` property. This means it's positioned relative to its closest ancestor element, which in this case is the `.container` element. The `top: 0` and `left: 0` properties ensure that the toolbar appears at the top left corner of the `.container` element.
At first, the toolbar is hidden from view with the `opacity: 0` property. But when text is selected within the text area, a JavaScript function can be used to change the opacity of the toolbar to 1 and display it on top of the text area. We'll cover this in the next section.
Here are the basic styles for the toolbar:
css
.container {
position: relative;
}
.container__toolbar {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
Creating the overlay and mirrored elements can be done dynamically, but it's also possible to create the toolbar in the markup and save ourselves from writing a lot of JavaScript code.
html
<div class="container" id="container">
<div id="toolbar" class="container__toolbar">
<!-- Buttons ... -->
</div>
<textarea id="textarea" class="container__textarea"></textarea>
</div>
Here's a sample code snippet that shows how we can move the toolbar from being a direct child of the container to being a direct child of the overlay:
js
const containerEle = document.getElementById('container');
const textarea = document.getElementById('textarea');
const toolbarEle = document.getElementById('toolbar');

const overlayEle = document.createElement('div');
overlayEle.classList.add('container__overlay');
containerEle.prepend(overlayEle);

// Move the toolbar
overlayEle.appendChild(toolbarEle);

const mirroredEle = document.createElement('div');
mirroredEle.textContent = textarea.value;
mirroredEle.classList.add('container__mirror');
overlayEle.appendChild(mirroredEle);
As you can see, it's a simple task that doesn't require anything fancy.

Showing the toolbar when text is selected

First, we need to listen for the `mouseup` event on the text area. This event will tell us when the user has selected text. Then, we can check if any text is currently selected within the text area by comparing the cursor index of the selected text. To get them, simply access the `selectionStart` and `selectionEnd` properties.
js
textarea.addEventListener('mouseup', () => {
const cursorPos = textarea.selectionStart;
const endSelection = textarea.selectionEnd;
if (cursorPos !== endSelection) {
// Users selected text ...

// Build the mirrored element ...
mirroredEle.append(pre, caretEle, post);
}
If there is selected text, we can build a mirrored element from three elements: two text nodes representing the text before and after the selected text, and an empty span element representing the starting cursor. This is the same technique you've already become familiar with in this series.
Now, let's talk about positioning the toolbar. We want it to appear above the selected text and centered horizontally with respect to the text area. To do this, we need to calculate the `top` and `left` properties of the toolbar element by calculating the bounding rectangles of both the caret and toolbar elements.
Here's a sample code to help you get started:
js
const rect = caretEle.getBoundingClientRect();
const toolbarRect = toolbarEle.getBoundingClientRect();
const left = (textarea.clientWidth - toolbarRect.width) / 2;
const top = rect.top + textarea.scrollTop - toolbarRect.height - 8;
In this example, the `rect` object gives us info about where the caret element is in relation to the viewport. We can use this, along with `textarea.scrollTop`, to figure out where to put the toolbar. To make sure the toolbar shows up right above the selected text, we subtract `toolbarRect.height` (the height of the toolbar element) and a small constant (like 8) from `rect.top`.
Now, to center the toolbar horizontally with respect to the textarea, we need to calculate how much space is available on either side of the textarea. We do this by subtracting `toolbarRect.width` (the width of the toolbar element) from `textarea.clientWidth` (the width of the textarea). Then, we divide this number by two to get half of the remaining space. Finally, we set `left` equal to this value, and voila! The toolbar is perfectly centered.
To move the toolbar element to a new position, we use the `transform` property. Specifically, we use the `translate()` function to adjust the toolbar's position. By setting the `left` and `top` values to calculated values, we can center the toolbar perfectly above the selected text.
js
toolbarEle.style.transform = `translate(${left}px, ${top}px)`;
Good practice
The `translate()` function takes two arguments: the horizontal and vertical distance to move. We pass in pixel values for these arguments to move the toolbar horizontally by a certain amount (`left`) and vertically by another amount (`top`). Using `transform` gives us smooth animations and transitions, thanks to hardware acceleration in modern browsers. Plus, we can avoid any layout recalculations that might happen if we tried to change the `top` or `left` properties directly.
Now that we have these calculations in place, our toolbar will always be optimally positioned when text is selected within our text area. Once we move the toolbar to the desired position, we can make it visible by setting the `opacity` and `visibility` properties to 1 and `visible`, respectively. To achieve this, we create the `showToolbar()` function.
js
const showToolbar = () => {
toolbarEle.style.opacity = 1;
toolbarEle.style.visibility = 'visible';
};

Hiding the toolbar when no text is selected

Let's talk about how to hide the toolbar when there's no text selected. We can make this happen by detecting when the user clears their text selection. To do this, we listen for the `selectionchange` event on the `document` object. This event is triggered whenever the user changes their text selection.
Inside the event handler, we first check if any text is currently selected by calling `window.getSelection().toString()`. If no text is selected, we simply call the `hideToolbar()` function.
js
document.addEventListener('selectionchange', () => {
const selection = window.getSelection().toString();
if (!selection) {
hideToolbar();
}
});
This function hides our toolbar by setting its `opacity` and `visibility` properties to 0 and `hidden`, respectively. It's worth noting that we set both properties to ensure that the toolbar doesn't interfere with users selecting text in a text area below it.
js
const hideToolbar = () => {
toolbarEle.style.opacity = 0;
toolbarEle.style.visibility = 'hidden';
};
By doing this, we ensure that our toolbar is hidden from view whenever there's no text selected within our textarea element.

Simplifying text formats

In this post, we'll make the editor simpler by providing only three buttons in the toolbar. These buttons allow users to make selected text bold, italic, or strike through. The toolbar contains only three buttons, as follows:
html
<div id="toolbar" class="container__toolbar">
<button class="toolbar__button" data-format="bold">...</button>
<button class="toolbar__button" data-format="italic">...</button>
<button class="toolbar__button" data-format="strike">...</button>
</div>
Each button comes with a special `data-format` attribute. When users click a button, we can determine which format they want to apply to the selected text.
To handle the format buttons, we'll query all of them and add a click event listener to each one. Inside the event listener, we'll use the `getAttribute()` method to retrieve the value of the `data-format` attribute and determine which format button was clicked.
js
[...toolbarEle.querySelectorAll('.toolbar__button')].forEach((button) => {
button.addEventListener('click', (e) => {
const format = button.getAttribute('data-format');
console.log(format);
});
});
To loop through all of our format buttons and attach a click event listener to each one, we use a combination of `querySelectorAll()` and `forEach()`. Once clicked, we retrieve the value of the `data-format` attribute by calling `getAttribute()`. This will give us either "bold", "italic", or "strike" depending on which button was clicked.

Making text bold in Markdown

To make text bold in markdown, simply wrap the selected text with double asterisks (**). This tells markdown to render the enclosed text as bold.
To add this functionality to our toolbar, we need to get the start and end positions of the selected text using `textarea.selectionStart` and `textarea.selectionEnd`. We also need to get the current value of the textarea using `textarea.value`.
Once we have this information, we can use the `setRangeText()` method to insert the double asterisks before and after the selected text. This method takes three arguments: a string representing the new text to be inserted, an integer representing the starting cursor position, and an integer representing the ending cursor position.
For example, to make a selection bold, we could use the following code:
js
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentValue = textarea.value;

// Insert double asterisks before and after selected text
textarea.setRangeText(`**${currentValue.slice(start, end)}**`, start, end);
After formatting text, we can enhance the user experience by automatically placing the cursor at the end of the newly formatted text. To do this, we simply call `focus()` on the textarea element to shift focus back to it, and then set the `selectionStart` property to be just after the end of our newly formatted text.
For example, if we make a selection bold in our toolbar, we can add the following code to move the cursor to just after our newly formatted text:
js
textarea.focus();
textarea.selectionStart = end + 4;
This code calls `focus()` on our textarea element to shift focus back to it. We then set `selectionStart` equal to `end + 4`, where `end` is the original end position of our selection. The number 4 represents the length of the double asterisks that were added during formatting.
By setting `selectionStart` in this way, we effectively place the cursor just after our newly formatted text. This ensures that users can continue typing or making further changes without having to manually reposition their cursor.
Implementing this feature helps make our toolbar more user-friendly and intuitive, ultimately improving the overall user experience.

Making text italic in Markdown

To make text italic in Markdown, simply wrap the selected text within a pair of asterisks (*). It's that easy! Here's an example:
js
textarea.setRangeText(`*${currentValue.slice(start, end)}*`, start, end);
textarea.focus();
textarea.selectionStart = end + 2;

Striking through text in Markdown

Just like making text italic, you can easily strike through text in Markdown. All you need to do is wrap the selected text within a pair of tildes, like this: selected text.
js
textarea.setRangeText(`~~${currentValue.slice(start, end)}~~`, start, end);
textarea.focus();
textarea.selectionStart = end + 4;
Let's take a look at the final demo!

See also

If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn