Mirror a text area for improving user experience
Phuoc Nguyen
Posted on December 15, 2023
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>
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);
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%;
}
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];
});
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;
}
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);
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);
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;
}
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;
});
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:
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
July 24, 2024