Expanding Text Areas Made Elegant
Issue № 338

Expanding Text Areas Made Elegant

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!#section2

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


Closing remarks#section4

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

Neil Jenkins

Neil Jenkins works for Opera Software, where he leads the UX design and front-end engineering for My Opera Mail. When not coding he can often be found singing, hiking through remote mountains, or quite possibly both.

29 Reader Comments

  1. Great post! I recently stumbled across this problem when designing a website. Solved it with one of those keyup solutions; was never really happy about it. This will definitely fix it! Thanks.

  2. I read this article on the bus commute, didn’t seem to be working on iPhone 4, pre iOS5 update. Excellent technique however. Looking forward to blasting the future with awesome textareas.

  3. I really like this technique – it is better than a lot of the others I have seen. I threw together a jQuery plugin version based on your code that limits the amount of extra CSS and handles some of the cross browser issues, while automatically initializing textareas with an ‘expanding’ class on load:

    “http://jsfiddle.net/bgrins/UA7ty”:http://jsfiddle.net/bgrins/UA7ty/

    The other thing it does is attempt to gather any possible CSS from the textarea for things like font and spacing when initializing the mirror ‘pre’ tag. This is helpful in case there are properties that you don’t remember or know about resetting for both the ‘pre’ and the ‘textarea’. Obviously, this doesn’t work for percentage paddings and widths, etc – it is still best to define rules for both at the same time, but it might make it a little easier to drop into an existing system if it handles CSS rules that cascaded down to either the textarea or pre.

    Note: I haven’t really tested this in IE. You mentioned the onpropertychange event not delegating properly: jQuery may or may not handle that transparently.

  4. @bgrins, thought about making a GitHub Repository for your plugin? I was thinking of making one for it, but I don’t want to copy your code if you are going to make one.

  5. I’m sure this would be an easy tweak in the code but it seems like if you’re going to have an expanding text area you should always have more than one line to start with so someone knows up front that they are able to put in more than one line of text, otherwise as they type the might cut it short before they realize that more lines can be added…

  6. @aBrentApart, a simple way to do this would be to add a min-height property to your CSS for the element that controls the height of the textarea.

    If you did a min-height: 2em; (adjust for your line-height) then it would start off with two rows of height and would start adjusting once you are over two lines.

  7. As @Zoram said, you can specify a min-height to make it initially 2 (or whatever) lines in height and then always fit exactly to the content as it expands past that. To make it always be one line taller than the content, just add an extra
    inside the

     element. Whether you want to do either of these things will depend on the circumstances; in some constrained places like a smartphone app, or where you have lots of text areas together in a small space you may want them to start at only 1 line's height. In other situations, you probably want them to be 2 or 3 lines in height to begin with to let the user know they can enter more than one line of text.
  8. IE9 may not, but I believe IE8 does require the ms prefix for the box-sizing property.

  9. Doesn’t seem to work. Replication: (type or copy and paste)

    “i am the waterman see what i can do queen of all that is right and soo the man with the can did what he can and saw what he saw but only if there was a law but there wasn’t and now”

    resizes after a couple more chars are entered.

  10. Good Post! Your post is an excellent example of why I keep comming back to read your excellent quality content that is forever updated. More please, this post helped me consider a few more things, keep up the good work.
    And I have a online fashion , if you have interest , welcome to go and have a look .

  11. @slapzstick. It appears Firefox (at least on Mac) is adding an extra 1px padding to the text area in addition to whatever you specify; this means with certain inputs (such as your example text), the text in the text area will wrap a word before the pre does, so it gets out of sync. As a workaround, you can add a Firefox-only padding property to the text area (this will be ignored by other browsers:

    padding: -moz-calc( 5px ) -moz-calc( 4px );

    That fixes it. Strange, and annoying, bug in Firefox though.

  12. Looks great, but what about accessibility? Wouldn’t it be confusing to people who use screen readers to have their text appear twice? (assuming that’s what would happen, i admit i do not know that much about accessibility software)

  13. Wow. Thank you very much for this amazing form enhancement! For anyone inclined to use jQuery, here is a jQuery version I derived from the Javascript used in this example. Of course all the HTML and CSS in the example would stay the same.

    $(function() {
    $(‘div.expandingArea’).each(function() {
    var area = $(‘textarea’, $(this));
    var span = $(‘span’, $(this));
    area.bind(‘input’, function() { span.text(area.val()); });
    span.text(area.val());
    $(this).addClass(‘active’);
    });
    });

  14. That’s pretty cool. I will try it today. Came in just as called for since I am actually playing with some website setup for smart phones right now. Could use it for contact page. Thanks for sharing.

  15. @kigorw This is the same problem @slapzstick came across; in Firefox (only), it appears there’s a hidden 1px extra horizontal padding added to textareas which you can’t remove. See my comment 17 for a suggested fix.

    @jsturgis Hmm, I’ve had it working fine in iOS4 before, but unfortunately I now only have iOS5 devices so can no longer test and see what’s happening there. My guess, given that you say you can’t adjust the padding, is that the styles on the textarea are being overridden to the platform defaults. Adding the CSS “-webkit-apparance: none” to the textarea might well fix it.

  16. @Neil, How about adding _text-indent: -moz-calc(-1px)_ to the textarea instead of altering the padding? Seems to work fine in Firefox 8 for Windows.

  17. Nice idea. Unfortunately, as 11% of the users of the sites I administer still use IE7 (9%) or IE6 (2%) I won’t be using it. However, I like the concept of breaking free from the bog standard textarea, so I will definately be bearing your work in mind in the future.

  18. When I run your version on IE it works fine. However, when I run my copied code version locally IE throws up errors for document object not having the method querySelectorsAll. Anything I might be overlooking?

  19. It looks like the demo broke some time in the last year (the quotes in the script have been replaced with their HTML entity equivalent).

Got something to say?

We have turned off comments, but you can see what folks had to say before we did so.

More from ALA

I am a creative.

A List Apart founder and web design OG Zeldman ponders the moments of inspiration, the hours of plodding, and the ultimate mystery at the heart of a creative career.
Career