// 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)
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 + ' '
})
})
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 }}
</div>
</div>