Mirror a text area for improving user experience
Tags: tutorial, javascript, webdev
Published: 2023-12-15 06:50:07

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:

```html
<div class="container" id="container">
    <textarea id="textarea" class="container__textarea"></textarea>
</div>
```

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.

```js
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);
```

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:

```css
.container {
    position: relative;
}
.container__textarea {
    position: relative;
}
.container__mirror {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
}
```

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:

```js
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];
});
```

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.

{% codepen https://codepen.io/phuocng/pen/PoVrpZq %}

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.

```css
.container__mirror {
    color: transparent;
}
```

## 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:

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

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()`.

```js
const parseValue = (v) => v.endsWith('px')
    ? parseInt(v.slice(0, -2), 10)
    : 0;
const borderWidth = parseValue(textareaStyles.borderWidth);
```

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.

```css
.container__mirror {
    overflow: hidden;
}
```

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.

```js
textarea.addEventListener('scroll', () => {
    mirroredEle.scrollTop = textarea.scrollTop;
});
```

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

{% codepen https://codepen.io/phuocng/pen/QWYXpNd %}

---

It's highly recommended that you visit the [original post](https://phuoc.ng/collection/mirror-a-text-area/mirror-a-text-area-for-improving-user-experience/) to play with the interactive demos. 

If you found this series helpful, please consider giving the [repository](https://github.com/phuocng/mirror-a-text-area) 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:

-   [Twitter](https://twitter.com/_phuocng)
-   [GitHub](https://github.com/phuocng)