Auto growing textareas

This post was originally made on the Assanka blog. Assanka was acquired by the Financial Times in January 2012, and became what is now FT Labs. Learn more.

This feels like a topic that’s been explored to death already, but I really don’t like most implementations of this technique, so here’s how we do it.

First, in case anyone has just arrived from Mars, or even more unlikely, isn’t familiar with Facebook, the auto-growing textarea is a text box that gets bigger as you type into it, so that you never see a scroll bar (unless you’re typing war and peace).

There are lots of ways of doing each part of this. The parts I’ll look at are (a) what triggers a review of the text box’s size, (b) how to determine whether a resize is required, and (c) how to perform a resize.

Triggering a review

There are four ways this is typically done:

  1. On an interval, say every 50ms
  2. On keypress
  3. On change
  4. On keyup

I’m staggered by how many scripts insist on doing this kind of thing on an interval. Setting up interval timers for no good reason is a shortcut to terrible performance and memory leaks galore. So we won’t be doing that. onkeypress and onchange events are triggered before the box is updated with the latest keypress, so we want to avoid those, as the latest keypress might have been the one to bring us onto a new line. That leaves onkeyup, which is fired after the box is updated with the new character, and allows us to inspect it and decide whether to increase its size.

To resize or not to resize

How to determine whether to resize the box comes in three flavours:

  1. Count the number of newline characters in the textarea’s value, and see if that matches the number of rows the textarea has.
  2. Create a ‘shadow’ DIV which is off screen (eg. margin-left: -10000px) and has no height declared, but otherwise has the same style properties as the textarea, fill it with the textarea’s content, measure the subsequent height of the DIV, then see if that height matches the current height of the textarea.
  3. Check whether scrollHeight (the height of the scollable content of the box) > clientHeight (the height of the box itself).

In this case I was suprised at the number of implementations that favoured counting newlines. This only works if combined with counting the number of characters in the textarea’s value, AND a monospaced font, AND knowing the number of columns in the textarea. In short, er, it’s mad.

Second option, and the one favoured by a lot of the framework plugins due to the ease with which you can create shadow elements in the likes of jQuery, is to create a shadow DIV. This has the advantage of telling you the actual pixel height of the text, even where it is LESS than the height of the textarea box. Otherwise, you’re limited to measuring clientHeight and scrollHeight, which are exactly the same if the textarea isn’t scrolling, regardless of how much space you’ve got to spare at the bottom. My issue with this method is that it basically requires use of a framework to not be painful, and even then it’s non-trivial amounts of code, and adds needless pollution to the DOM.

So that leaves relying on scrollHeight and clientHeight. These are well supported and efficient to read, so provided that you work around the issue of scrollHeight always being at least equal to clientHeight, this offers a very lightweight solution. The features that you can achieve with a shadow DIV that you can’t do by simply reading scrollHeight and clientHeight are (a) you can ensure there is always a blank line at the bottom of your textarea, and (b) you can shrink the textarea if the user deletes text from it. I’m personally of the view that neither of these is actually particularly desirable. There’s potentially an argument for leaving a blank line at the bottom, but equally the uer might feel like they’re just being pressured to write more.

How to resize the box

OK, so if you’ve concluded that the user is writing chapter and verse and the textarea is in need of a bit more space, how do we go about doing it?

  1. Animate the CSS height property
  2. Add some height via the CSS height property
  3. Add 1 to the rows attribute

A fair few of the framework plugins use their framework’s animation capabilities to animate the grow effect. I don’t like this. Just because you have an animation effect available doesn’t mean it’s appropriate to use it, and there are many situations where the end user just doesn’t want to wait 300ms for the privilege of using a fresh line.

Simply adding height to the CSS property is a fair way of doing it, but unless you do some maths on the user’s line height (or hard code some magic numbers), you can’t necessarily guarantee that the resulting height will be a multiple of the textarea’s line height.

Easiest, most efficient solution: add one to the rows attribute of the textarea. rows is part of XHTML as well as HTML 4.01, and has universal support going back yonks.

Pasting

Watch out for pastes. If the user pastes in a large quantity of text, they will trigger only one keyup event, but will have added many lines to the textarea. Make sure that if a resize is required, you trigger another review after the resize is complete.

Max height: the war and peace scenario

If the user really does seem to be writing a novel, we probably should call it a day on growing the textarea at some stage, and certainly before it gets to be taller than the viewport. You can check the height against the viewport height, though I typically just restrict it to an arbitrary height, say 30 rows.

The code

You’ll need a textarea element with an ID of mytextarea to make this code sample work, and obviously you can easily modify it to use selectors from your favourite framework rather than the native document.getElementById.

document.getElementById('mytextarea').onkeyup = function() {
	var ta = document.getElementById('mytextarea');
	var maxrows = 30;
	var lh = ta.clientHeight / ta.rows;
	while (ta.scrollHeight > ta.clientHeight && !window.opera && ta.rows < maxrows) {
		ta.style.overflow = 'hidden';
		ta.rows += 1;
	}
	if (ta.scrollHeight > ta.clientHeight) ta.style.overflow = 'auto';
}