home about me

Easy Auto Sizing Textarea

Two options for creating a textarea field that grows and shrinks with user input. Easily definable minimum and maximum height. Example code for Javascript and Vue.js.

Vanilla Javascript

The most straightforward way is to simply calculate the height of the content inside the textarea:


// can be set to `null` to disable
const MIN_HEIGHT = 70
const MAX_HEIGHT = 200

// takes an HTMLElement as argument
function resizeTextarea(textareaElement) {
  // set height to auto to let the browser calculate the true content height
  textareaElement.style.height = 'auto'
  // wait for one single frame so the textarea field got rendered
  window.requestAnimationFrame(() => {
    // we need to add one pixel because of Firefox shenanigans
    let contentHeight = textareaElement.scrollHeight + 1
    // abide to minimum height
    contentHeight = Math.max(contentHeight, MIN_HEIGHT)
    // abide to maximum height, if given
    if (MAX_HEIGHT) {
      contentHeight = Math.min(contentHeight, MAX_HEIGHT)
    }
    // we literally just set the outer height to the (clamped) height of the content
    textareaElement.style.height = `${contentHeight}px`
  })
}

// bind it to the `input` event, call the function once to initialise, done!
const myTextarea = document.getElementById('js-textarea-autogrow')
myTextarea.addEventListener('input', (event) => resizeTextarea(event.target))
resizeTextarea(myTextarea)

Smart (JS + CSS) Way

With some extra elements and a bit of CSS, we can cut down on the amount of Javascript and prevent the need for the rendering delay:

 

Here, a hidden dummy element with the same content as the textarea is used to let the layout engine do the calculations:

<div class="c-autosize">
  <textarea class="c-autosize__textarea"></textarea>
  <div class="c-autosize__dummy"></div>
</div>

It requires a bit of CSS:

.c-autosize {
  /* using the grid, we position the textarea over the dummy */
  display: grid;
  /* set the width on the parent element */
  width: 250px;
}

.c-autosize__dummy {
  /* ignore clicks on dummy */
  pointer-events: none;
  /* the dummy should be invisible */
  visibility: hidden;
  /* make long lines of text behave like inside a textarea field */
  white-space: pre-wrap;
}

.c-autosize__textarea,
.c-autosize__dummy {
  /* both elements span the whole grid */ 
  grid-area: 1 / 1 / 2 / 2;
  /* max and min values are optional */
  max-height: 200px;
  min-height: 70px;
  
  /* make sure both elements use the same font styling */
  font-size: inherit;
  line-height: inherit;

  /* disable manual resizing of the textarea field */
  resize: none;
}

And only a tiny bit of Javascript:

// iterate over all .c-autosize elements
document
  .querySelectorAll('.c-autosize')
  .forEach((autosizeEl) => {
    // store the reference to the dummy <div>
    const dummyElement = autosizeEl.querySelector('.c-autosize__dummy');
    // add input event handler to the textarea field
    autosizeEl
      .querySelector('.c-autosize__textarea')
      .addEventListener('input', (event) => {
        // set dummy element's content to what is typed inside the textarea,
        // adding a single non-breaking space to the end to prevent layout shifts
        dummyElement.innerHTML = event.target.value + '&nbsp;'
      })
  })

The Javascript and HTML parts could easily be combined in Vue, for example:

<div class="c-autosize">
  <textarea
    v-model="inputText"
    class="c-autosize__textarea"/>
  <div class="c-autosize__dummy">
    {{ inputText }}
    &nbsp;
  </div>
</div>