Earlier this year I wrote a bit about autogrowing textareas and inputs. The idea was to make a
<textarea> more like a
<div> so it expands in height as much as it needs to in order to contain the current value. It’s almost weird there isn’t a simple native solution for this, isn’t it? Looking back at that article, none of my ideas were particularly good. But Stephen Shaw’s idea that I linked to toward the end of it is actually a very good idea for this, so I wanted to shine some light on that and talk through how it works, because it seems like it’s the final answer to how this UX can be done until we get something native and better.
Here’s the demo in case you just want a working example:
The trick is that you exactly replicate the content of the
<textarea> in an element that can auto expand height, and match its sizing.
So you’ve got a
<textarea>, which cannot auto expand height.
Instead, you exactly replicate the look, content, and position of the element in another element. You hide the replica visually (might as well leave the one that’s technically-functional visible).
Now all three elements are tied to each other. Whichever of the children is tallest is will push the parent to that height, and the other child will follow. This means that the minimum height of the
<textarea> will become the “base” height, but if the replicated text element happens to grow taller, everything will grow taller with it.
So clever. I love it so much.
Same font, same padding, same margin, same border… everything. It’s an identical copy, just visually hidden with
visibility: hidden;. If it’s not exactly the same, everything won’t grow together exactly right.
We also need
white-space: pre-wrap; on the replicated text because that is how textareas behave.
In my demo, I’m using
::after for the replicated text. I’m not sure if that’s the best possible approach or not. It feels clean to me, but I wonder if using a
<div aria-hidden="true"> is safer for screen readers? Or maybe the
visibility: hidden; is enough for that? Anyway, that’s not the weird part. This is the weird part:
content: attr(data-replicated-value) " ";
Because I am using a pseudo-element, that’s the line that takes the
data attribute off the element and renders the content to the page with that extra space (that’s the weird part). If you don’t do that, the end result feels “jumpy.” I can’t say I entirely understand it, but it seems like it respects the line break behavior across the textarea and text elements better.
If you don’t want to use a pseudo-element, hey, fine with me, just watch for the jumpy behavior.
Special high fives to Will Earp and Martin Tillmann who both randomly emailed on the same exact day to remind me how clever Shaw’s technique is. Here’s an example Martin made with Alpine.js and Tailwind that also ends up kinda like a one-liner (but note how it’s got the jumpy thing going on).
I’m sure ya’ll could imagine how to do this with Vue and React and whatnot in a way that can very easily maintain state across a textarea and another element. I’m not going to include examples here, partially because I’m lazy, but mostly because I think you should understand how this works. It will make you smarter and understand your site better.