A List Apart

Menu
Issue № 338

Expanding Text Areas Made Elegant

by Published in HTML, JavaScript, Interaction Design · 28 Comments

An expanding text area is a multi-line text input field that expands in height to fit its contents. This UI element is commonly found in both desktop and mobile applications, such as the SMS composition field on the iPhone. Examples can also be found on the web, including on Facebook, where it’s used extensively. It’s a good choice wherever you don’t know how much text the user will write and you want to keep the layout compact; as such, it’s especially useful on interfaces targeted at smartphones.

Article Continues Below

Despite the ubiquity of this control, there’s no way to create it using only HTML and CSS. While normal block-level elements (like a div, for example) expand to fit their content, the humble textarea does not, even if you style it as display: block. Since this is the only way to accept multi-line user input (other than using contenteditable, a whole world of pain I’m not going to dive into today), a little JavaScript is needed to make it resize as desired.

Trawling the internet, you can find several attempts at creating expanding text areas, but most suffer from one or more of the following problems:

  • The height is calculated by guessing where wrapping occurs based on the cols attribute. This breaks if you set the width of the textarea in CSS, use a proportional width font, or simply don’t supply a cols attribute.
  • The height is recalculated and set only on the keyup (and possibly cut/paste) events. This doesn’t work if the textarea has a fluid width and the window is resized.
  • The required height is calculated based on the scrollHeight attribute, which is not specified in any W3C spec (it was introduced by IE) and thus unsurprisingly has subtle differences between implementations, requiring an inelegant and brittle lump of browser-sniffing code.

The best solution I’ve seen uses a separate pre element absolutely positioned off screen, styled the same as the textarea; let’s call this the mirror element. Using setTimeout, the textarea is then polled every 200ms or so, and each time a new value is found, the content of the mirror element is updated. This then automatically sizes to fit its content (as a normal block-level element does), after which you can extract the size from the offsetHeight property and apply that back to the textarea.

This method works, but the polling is inefficient, especially if you have multiple text areas. Worse, if you support flexible-width text areas you must check that the width of the textarea hasn’t changed on each poll as well (an expensive read to offsetWidth). It can be tricky to calculate the exact width of the content-box in the textarea; hence there’s normally a “fudge factor” added to the height applied to the textarea, just to make sure it’s not slightly too short, resulting in a box that’s then slightly too big for the content. Here, I’m going to show you a better solution to the problem, which sizes the textarea  using only the smallest snippet of JavaScript magic along with some cunning CSS.

The technique is an improvement on the offscreen-positioned mirror element. The first improvement we make is related to how we detect input. The change event is not ideal as it only fires when the textarea loses focus. The keyup event works most of the time, but also fires on events where no real change has been made, such as moving the cursor left and right; and it does not fire if the user uses the mouse to cut or paste. What we really want is an event that simply fires whenever the value of the textarea changes. Fortunately, such an event exists and it’s incredibly useful, although it’s mentioned so rarely it would seem many are unaware of its existence. The event is simply called input, and you use it just like any other event:

textarea.addEventListener('input', function (event) {
    /* Code to handle event */
}, false );

So our first improvement is to stop polling using setTimeout and instead use the much more efficient input event. This is supported cross-browser, even in Internet Explorer from version 9, although of course there is an IE bug: it doesn’t fire when you delete text, so the area will not shrink until text is added again. If this is a concern you can also watch for the keyup event in IE to cover most cases.

For IE8 we can use the proprietary onpropertychange event, which also fires whenever the value property changes. This event is also available on versions less than 8, but a few small CSS tweaks will probably be needed to make the expanding text area work overall; I leave making it work in IE6 or IE7 as an exercise to the readers unlucky enough to have to support those ancient browsers.

Now, some of you may have spotted that, as we’re no longer polling, the textarea won’t resize if it has a fluid width and the window is resized. That brings us to our next improvement: we’re going to make the browser resize the textarea automatically. But, I hear you cry, I thought you said this was impossible? Well, not quite. You can’t do it automatically with just HTML and CSS, but all the JS needs to do is update the mirror element with the value of the textarea. It doesn’t have to measure or explicitly set height. The trick is to position the textarea on top of the mirror element, both inside a relatively-positioned containing div. The textarea is positioned absolutely and given a width and height of 100% to make it fill the div. The mirror is positioned statically so it will expand to fit its contents. The containing div will then expand to fit the height of the mirror and this in turn will make the absolutely positioned textarea resize to fill the container, thus making it the perfect height to fit its contents.

Enough explanation, just give me the code!

Whoa there! I’m just getting to that. It’s really beautifully simple. The markup looks like this:

<div class="expandingArea">
  <pre><span></span><br></pre>
  <textarea></textarea>
</div>

The pre is our mirror. We need a br at the end of it to ensure that any trailing whitespace copied from the textarea is rendered by the browser correctly and not chewed up. The span element is therefore the one we actually update with the contents of the textarea.

Now, the CSS. First, a tiny reset; (you probably already have this):

textarea, 
pre {
  margin: 0;
  padding: 0;
  outline: 0;
  border: 0;
}

Containing elements have the expandingArea class. You can define any border or inset box-shadow etc., here that you want to use to style your text area. I’ve just added a simple 1px solid gray border. You can also set a min-height property if you want and it will work as expected:

.expandingArea {
  position: relative;
  border: 1px solid #888;
  background: #fff;
}

You can set any padding, line height, and font styles you like, just make sure they’re the same for both the textarea and the pre element:

.expandingArea > textarea,
.expandingArea > pre {
  padding: 5px;
  background: transparent;
  font: 400 13px/16px helvetica, arial, sans-serif;
  /* Make the text soft-wrap */
  white-space: pre-wrap;
  word-wrap: break-word;
}
.expandingArea > textarea {
  /* The border-box box model is used to allow
   * padding whilst still keeping the overall width
   * at exactly that of the containing element.
   */
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
      -ms-box-sizing: border-box;
          box-sizing: border-box;
  width: 100%;
  /* This height is used when JS is disabled */
  height: 100px;
}
.expandingArea.active > textarea {
  /* Hide any scrollbars */
  overflow: hidden;
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  /* Remove WebKit user-resize widget */
  resize: none;
}
.expandingArea > pre {
  display: none;
}
.expandingArea.active > pre {
  display: block;
  /* Hide the text; just using it for sizing */
  visibility: hidden;
}

And lastly we use the following JavaScript. For brevity I’ve omitted the usual feature detection you should do before using querySelector() or querySelectorAll(). In the best tradition of graceful degradation, users without JavaScript enabled will get a fixed height textarea (with the height set in the CSS), with scrollbars appearing when the content overflows:

function makeExpandingArea(container) {
 var area = container.querySelector('textarea');
 var span = container.querySelector('span');
 if (area.addEventListener) {
   area.addEventListener('input', function() {
     span.textContent = area.value;
   }, false);
   span.textContent = area.value;
 } else if (area.attachEvent) {
   // IE8 compatibility
   area.attachEvent('onpropertychange', function() {
     span.innerText = area.value;
   });
   span.innerText = area.value;
 }
// Enable extra CSS
container.className += "active";
}var areas = document.querySelectorAll('.expandingArea');
var l = areas.length;while (l--) {
 makeExpandingArea(areas[l]);
}

A note on delegation: you can easily set this up with a single event listener on the document node and use event delegation to handle multiple expanding text areas efficiently, but only if you’re not supporting IE8 or below, as Microsoft, in their infinite wisdom, did not make the onpropertychange event bubble.

The obligatory demo

Closing remarks

Due to the way redraws are batched in Opera for Mac OS X, a slight flicker may occur when a new line is added to the text field. You can work around this by always making it a line taller than it needs to be in Opera on Mac; just add the following code to the top of the makeExpandingArea function (sadly there’s no way to do feature detection for this):

if ( window.opera && /Mac OS X/.test( navigator.appVersion ) ) {
  container.querySelector( 'pre' )
           .appendChild(
    document.createElement( 'br' )
  );
}

Lastly, because the textarea is positioned directly over the pre, you can extend this to do funky stuff such as syntax highlighting as you type; if you parse the value and split it into different tags before adding it to the pre, you can apply different colors to different sections. You’ll need to remove the visibility: hidden declaration from the pre and instead add color: transparent to the textarea. We use this technique in My Opera Mail to make it easier to scan the names in the To/Cc/Bcc fields of the compose screen. The snag is that all browsers other than Opera make the cursor color the same as the text color, so the cursor disappears when you make the color transparent. I don’t believe any W3C standard covers this (please let me know if I’m wrong), but the platform standard (based on the text editors shipped with them) seems to be the inverse of the background color on Windows and always black on Mac, regardless of background or foreground color. But until the other browsers see the light and fix this behavior you can still apply the syntax highlighting on blur and turn it off while the user is actually editing, or change the background color instead.

And that’s it folks! Hope you’ve enjoyed reading this article and maybe learned a useful new technique. It’s elegant and efficient and works just as well in modern mobile browsers as on the desktop. Happy hacking!

About the Author

28 Reader Comments

Load Comments