Mirror a text area for improving user experience

phuocng

Phuoc Nguyen

Posted on December 15, 2023

Mirror a text area for improving user experience

In this post, we'll explore a technique for mirroring a text area to improve user experience. Instead of interacting directly with the text area, we'll clone it with a div element. Although the div element appears invisible, it enables us to perform special tasks that aren't possible with the original text area.

Cloning a text area

To clone the text area element with a div element, we'll need to make a few adjustments to the markup. Let's put the text area inside a container so we can position the div element later.

Here's what our layout should look like:

<div class="container" id="container">
    <textarea id="textarea" class="container__textarea"></textarea>
</div>
Enter fullscreen mode Exit fullscreen mode

To add the new div element to the container, we first need to get references to both elements using the getElementById method.

The new div element is prepended to the container so that it sits below the text area. Users can still interact with the text area, such as updating its content or resizing it.

const containerEle = document.getElementById('container');
const textarea = document.getElementById('textarea');

const mirroredEle = document.createElement('div');
mirroredEle.classList.add('container__mirror');
mirroredEle.textContent = textarea.value;
containerEle.prepend(mirroredEle);
Enter fullscreen mode Exit fullscreen mode

To position the container__mirror div element below the text area and make it cover the entire container, we need to make a few adjustments.

First, we'll set its position to absolute. This allows us to position the element relative to its containing block, which, in this case, is the container div.

Next, we'll set the top and left properties to 0, which positions the element at the top left corner of its containing block. To make sure the element takes up all available space within its containing block, we'll set the height and width properties to 100%.

With these adjustments, the container__mirror div will be perfectly positioned below the text area and fill the entire container.

Here's an example of how we might do this using CSS:

.container {
    position: relative;
}
.container__textarea {
    position: relative;
}
.container__mirror {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
}
Enter fullscreen mode Exit fullscreen mode

It's worth noting that we've set the position property of the text area to relative. This allows users to interact with and update the content of the text area.

Copying the styles

To make the div element look the same as the text area, we need to copy some styles from the text area element. We can do this by using the getComputedStyle() method, which returns a CSSStyleDeclaration object containing all styles for an element after applying any active stylesheets and resolving any basic computation those values may contain.

First, we get a reference to the text area element and store its computed styles in a variable called textareaStyles. Then, we loop through an array of properties that we want to copy over to the mirrored element, such as border, fontFamily, fontSize, lineHeight, and padding. For each property in our array, we set its value on the mirrored element's style object by accessing it with bracket notation using the current property name.

Here's an example of how we might do this:

const textareaStyles = window.getComputedStyle(textarea);
[
    'border',
    'boxSizing',
    'fontFamily',
    'fontSize',
    'fontWeight',
    'letterSpacing',
    'lineHeight',
    'padding',
    'textDecoration',
    'textIndent',
    'textTransform',
    'whiteSpace',
    'wordSpacing',
    'wordWrap',
].forEach((property) => {
    mirroredEle.style[property] = textareaStyles[property];
});
Enter fullscreen mode Exit fullscreen mode

When it comes to styling text, some things are obvious, like font-size and line-height. But other styles may not be so clear at first glance. That's where white-space, word-spacing, and word-wrap come in - these styles are essential for making sure that the mirrored text looks and acts just like the original.

white-space determines how white space characters (like spaces, tabs, and line breaks) are handled within an element. By copying this property from the original text area to the mirrored element, we make sure that any white space entered by the user appears consistently in both elements.

word-spacing controls the amount of space between words in an element. This might seem like a small detail, but it actually has a big impact on how easy the text is to read. By copying this property from the original text area to the mirrored element, we ensure that the space between words is consistent across both elements.

Finally, word-wrap determines whether long words can break onto multiple lines within an element. If this property is set to break-word, then long words will be broken at arbitrary points to fit within their container. By copying this property from the original text area to the mirrored element, we make sure that long words are handled consistently in both elements.

By doing this, we ensure that every single words of the mirrored element are displayed at the same positions as they are in the text area. This is crucial for creating a visually appealing and consistent user experience.

Let's take a look at how the mirror and text area elements appear at the same time. At first glance, it seems like every word of both elements is displayed in the same position, which is exactly what we want.

However, if we try to resize the text area by dragging the bottom-right corner, we run into some problems. First, the mirrored element isn't resized accordingly, and as a result, its content appears below the text area.

Even more problematic, if there's a scrollbar inside the text area, scrolling doesn't affect the mirrored element at all. But don't worry, we'll fix these issues in the next section.

Hiding the reflected text

Have you ever noticed that the reflected text in a text area looks blurry? This happens because each word is a combination of two single words in the reflected and main text areas. Luckily, there's a simple solution to this issue. With just one line of code, we can set the text color of the reflected text to transparent.

.container__mirror {
    color: transparent;
}
Enter fullscreen mode Exit fullscreen mode

Keeping track of text area size

Another issue we need to address is resizing the text area. Whenever users resize the text area, we need to update the size of the div element accordingly.

To tackle this problem, we can use the ResizeObserver API. This API provides a way to listen for changes to the dimensions of an element and take action when those changes occur.

To implement this, we can create a new instance of ResizeObserver and pass it a callback function that will be called whenever the size of the text area changes. In this case, we want to update the size of the mirrored element to match the new size of the text area.

Here's an example of how we can use the ResizeObserver API:

const ro = new ResizeObserver(() => {
    mirroredEle.style.width = `${textarea.clientWidth + 2 * borderWidth}px`;
    mirroredEle.style.height = `${textarea.clientHeight + 2 * borderWidth}px`;
});
ro.observe(textarea);
Enter fullscreen mode Exit fullscreen mode

In this example, we're creating a new ResizeObserver instance. We're passing it a callback function that will be called every time the size of the text area changes. Inside this function, we're finding the width of the text area by adding up the clientWidth property and its border width.

You can determine the border width from the computed styles. To extract the numeric value of a given property (if it exists), you can use a helper function like parseValue().

const parseValue = (v) => v.endsWith('px')
    ? parseInt(v.slice(0, -2), 10)
    : 0;
const borderWidth = parseValue(textareaStyles.borderWidth);
Enter fullscreen mode Exit fullscreen mode

We set the width property of the mirrored element to the result, and do the same for the height property using a similar approach.

But wait, we're not done yet! If you drag the bottom-right corner of the text area up to the top, you'll notice that the mirrored element positions are also updated. However, if you scroll up or down, they don't match with the original content in the text area anymore.

Keeping scroll positions in sync

When working with a text area that has a lot of content, you may notice that scrolling up or down causes the mirrored element to stay in place. To fix this, we need to sync the scroll positions between the text area and the mirrored element.

First, we can disable the scrollbar in mirrored element by setting the overflow property to hidden. This ensures that the content of the mirrored element stay in place and don't move around as the user scrolls through the content.

.container__mirror {
    overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

Next, we add an event listener to the text area that listens for the scroll event. When this event fires, we update the scrollTop property of the mirrored element to match the scrollTop property of the text area. This keeps both elements in sync and ensures that the mirror move with the text as the user scrolls.

textarea.addEventListener('scroll', () => {
    mirroredEle.scrollTop = textarea.scrollTop;
});
Enter fullscreen mode Exit fullscreen mode

By implementing this solution, our code now updates both elements in real-time, creating a smoother user experience.


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series 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!

If you want more helpful content like this, feel free to follow me:

💖 💪 🙅 🚩
phuocng
Phuoc Nguyen

Posted on December 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related