30

I'm trying to do a simple auto-expanding textarea. This is my code:

textarea.onkeyup = function () {
  textarea.style.height = textarea.clientHeight + 'px';
}

But the textarea just keeps growing indefinitely as you type...

I know there is Dojo and a jQuery plugin for this, but would rather not have to use them. I looked at their implementation, and was initially using scrollHeight but that did the same thing.

You can start answering and play with the textarea for your answer to play with.

Devin Rhode
  • 20,940
  • 6
  • 51
  • 67
  • I edited a typo in the code, referencing the variable 'report' was a mistake I made just editing here for my question. It wasn't the cause of the infinitely growing textarea – Devin Rhode Oct 12 '11 at 19:54
  • Duplicate: http://stackoverflow.com/questions/2924655/how-do-i-make-an-expanding-textbox – Anderson Green Dec 30 '12 at 06:53
  • Possible duplicate of [Creating a textarea with auto-resize](https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize) – localhostdotdev May 09 '19 at 16:00
  • duplicate of https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize (30+ answers) – localhostdotdev May 09 '19 at 16:00

6 Answers6

63

Reset the height before Using scrollHeight to expand/shrink the textarea correctly. Math.min() can be used to set a limit on the textarea's height.

Code:

var textarea = document.getElementById("textarea");
var heightLimit = 200; /* Maximum height: 200px */

textarea.oninput = function() {
  textarea.style.height = ""; /* Reset the height*/
  textarea.style.height = Math.min(textarea.scrollHeight, heightLimit) + "px";
};

Fiddle: http://jsfiddle.net/gjqWy/155

Note: The input event is not supported by IE8 and earlier. Use keydown or keyup with onpaste and/or oncut if you want to support this ancient browser as well.

Rob W
  • 328,606
  • 78
  • 779
  • 666
  • +1: Nice... I was looking for a reliable way to do this. Does this account for the little nuances too, like pasting into the textarea, etc.? – James Johnson Oct 12 '11 at 21:59
  • FYI - I've posted a solution that allows you to limit by rows instead of pixels: http://stackoverflow.com/a/24824750/126574 – VitalyB Jul 18 '14 at 12:14
9

I've wanted to have the auto-expanding area to be limited by rows number (e.g 5 rows). I've considered using "em" units, for Rob's solution however, this is error-prone and wouldn't take account stuff like padding, etc.

So this is what I came up with:

var textarea = document.getElementById("textarea");
var limitRows = 5;
var messageLastScrollHeight = textarea.scrollHeight;

textarea.oninput = function() {
    var rows = parseInt(textarea.getAttribute("rows"));
    // If we don't decrease the amount of rows, the scrollHeight would show the scrollHeight for all the rows
    // even if there is no text.
    textarea.setAttribute("rows", "1");

    if (rows < limitRows && textarea.scrollHeight > messageLastScrollHeight) {
        rows++;
    } else if (rows > 1 && textarea.scrollHeight < messageLastScrollHeight) {
        rows--;
    }

    messageLastScrollHeight = textarea.scrollHeight;
    textarea.setAttribute("rows", rows);
};

Fiddle: http://jsfiddle.net/cgSj3/

Community
  • 1
  • 1
VitalyB
  • 11,711
  • 8
  • 69
  • 91
3

For those interested in a jQuery version of Rob W's solution:

var textarea = jQuery('.textarea');
textarea.on("input", function () {
    jQuery(this).css("height", ""); //reset the height
    jQuery(this).css("height", Math.min(jQuery(this).prop('scrollHeight'), 200) + "px");
});
av av
  • 81
  • 8
1

...and if you need an infinitely expanding textarea (as I did), just do this:

var textarea = document.getElementById("textarea");

textarea.oninput = function() {
  textarea.style.height = ""; /* Reset the height*/
  textarea.style.height = textarea.scrollHeight + "px";
};
Colin R. Turner
  • 1,215
  • 14
  • 23
0

Unlike the accepted answer, my function cares about padding-{top,bottom} and border-{top,bottom}-width. And it has many parameters. Note that it doesn't set window.addEventListener('resize')

Function:

// @author Arzet Ro, 2021 <arzeth0@gmail.com>
// @license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// @source https://stackoverflow.com/a/70341077/332012
// Useful for elements with overflow-y: scroll and <textarea>
// Tested only on <textarea> in desktop Firefox 95 and desktop Chromium 96.
export function autoResizeScrollableElement (
    el: HTMLElement,
    {
        canShrink = true,
        minHeightPx = 0,
        maxHeightPx,
        minLines,
        maxLines,
    }: {
        canShrink?: boolean,
        minHeightPx?: number,
        maxHeightPx?: number,
        minLines?: number,
        maxLines?: number,
    } = {}
): void
{
    const FN_NAME = 'autoResizeScrollableElement'
    if (
        typeof minLines !== 'undefined'
        && minLines !== null
        && Number.isNaN(+minLines)
    )
    {
        console.warn(
            '%O(el=%O):: minLines (%O) as a number is NaN',
            FN_NAME, el, minLines
        )
    }
    if (
        typeof maxLines !== 'undefined'
        && maxLines !== null
        && Number.isNaN(+maxLines)
    )
    {
        console.warn(
            '%O(el=%O):: maxLines (%O) as a number is NaN',
            FN_NAME, el, maxLines
        )
    }
    canShrink = (
        canShrink === true
        ||
        // @ts-ignore
        canShrink === 1 || canShrink === void 0 || canShrink === null
    )

    const style = window.getComputedStyle(el)
    const unpreparedLineHeight = style.getPropertyValue('line-height')
    if (unpreparedLineHeight === 'normal')
    {
        console.error('%O(el=%O):: line-height is unset', FN_NAME, el)
    }
    const lineHeightPx: number = (
        unpreparedLineHeight === 'normal'
        ? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
        : parseFloat(unpreparedLineHeight)
    )

    // @ts-ignore
    minHeightPx = parseFloat(minHeightPx || 0) || 0
    //minHeight = Math.max(lineHeightPx, parseFloat(style.getPropertyValue('min-height')))
    // @ts-ignore
    maxHeightPx = parseFloat(maxHeightPx || 0) || Infinity
    minLines = (
        minLines
        ? (
            Math.round(+minLines || 0) > 1
            ? Math.round(+minLines || 0)
            : 1
        )
        : 1
    )
    maxLines = (
        maxLines
        ? (Math.round(+maxLines || 0) || Infinity)
        : Infinity
    )
    //console.log('%O:: old ov.x=%O ov.y=%O, ov=%O', FN_NAME, style.getPropertyValue('overflow-x'), style.getPropertyValue('overflow-y'), style.getPropertyValue('overflow'))
    /*if (overflowY !== 'scroll' && overflowY === 'hidden')
    {
        console.warn('%O:: setting overflow-y to scroll', FN_NAME)
    }*/
    if (minLines > maxLines)
    {
        console.warn(
            '%O(el=%O):: minLines (%O) > maxLines (%O), '
            + 'therefore both parameters are ignored',
            FN_NAME, el, minLines, maxLines
        )
        minLines = 1
        maxLines = Infinity
    }
    if (minHeightPx > maxHeightPx)
    {
        console.warn(
            '%O(el=%O):: minHeightPx (%O) > maxHeightPx (%O), '
            + 'therefore both parameters are ignored',
            FN_NAME, el, minHeightPx, maxHeightPx
        )
        minHeightPx = 0
        maxHeightPx = Infinity
    }
    const topBottomBorderWidths: number = (
        parseFloat(style.getPropertyValue('border-top-width'))
        + parseFloat(style.getPropertyValue('border-bottom-width'))
    )
    let verticalPaddings: number = 0
    if (style.getPropertyValue('box-sizing') === 'border-box')
    {
        verticalPaddings += (
            parseFloat(style.getPropertyValue('padding-top'))
            + parseFloat(style.getPropertyValue('padding-bottom'))
            + topBottomBorderWidths
        )
    }
    else
    {
        console.warn(
            '%O(el=%O):: has `box-sizing: content-box`'
            + ' which is untested; you should set it to border-box. Continuing anyway.',
            FN_NAME, el
        )
    }
    const oldHeightPx = parseFloat(style.height)
    if (el.tagName === 'TEXTAREA')
    {
        el.setAttribute('rows', '1')
        //el.style.overflowY = 'hidden'
    }
    // @ts-ignore
    const oldScrollbarWidth: string|void = el.style.scrollbarWidth
    el.style.height = ''

    // Even when there is nothing to scroll,
    // it causes an extra height at the bottom in the content area (tried Firefox 95).
    // scrollbar-width is present only on Firefox 64+,
    // other browsers use ::-webkit-scrollbar
    // @ts-ignore
    el.style.scrollbarWidth = 'none'

    const maxHeightForMinLines = lineHeightPx * minLines + verticalPaddings // can be float
    // .scrollHeight is always an integer unfortunately
    const scrollHeight = el.scrollHeight + topBottomBorderWidths
    /*console.log(
        '%O:: lineHeightPx=%O * minLines=%O + verticalPaddings=%O, el.scrollHeight=%O, scrollHeight=%O',
        FN_NAME, lineHeightPx, minLines, verticalPaddings,
        el.scrollHeight, scrollHeight
    )*/
    const newHeightPx = Math.max(
        canShrink === true ? minHeightPx : oldHeightPx,
        Math.min(
            maxHeightPx,
            Math.max(
                maxHeightForMinLines,
                Math.min(
                    Math.max(scrollHeight, maxHeightForMinLines)
                    - Math.min(scrollHeight, maxHeightForMinLines) < 1
                    ? maxHeightForMinLines
                    : scrollHeight,
                    (
                        maxLines > 0 && maxLines !== Infinity
                        ? lineHeightPx * maxLines + verticalPaddings
                        : Infinity
                    )
                )
            )
        )
    )
    // @ts-ignore
    el.style.scrollbarWidth = oldScrollbarWidth
    if (!Number.isFinite(newHeightPx) || newHeightPx < 0)
    {
        console.error(
            '%O(el=%O):: BUG:: Invalid return value: `%O`',
            FN_NAME, el, newHeightPx
        )
        return
    }
    el.style.height = newHeightPx + 'px'
    //console.log('%O:: height: %O → %O', FN_NAME, oldHeightPx, newHeightPx)
    /*if (el.tagName === 'TEXTAREA' && el.scrollHeight > newHeightPx)
    {
        el.style.overflowY = 'scroll'
    }*/
}

Usage with React (TypeScript):

<textarea
    onKeyDown={(e) => {
        if (!(e.key === 'Enter' && !e.shiftKey)) return true
        e.preventDefault()
        // send the message, then this.scrollToTheBottom()
        return false
    }}
    onChange={(e) => {
        if (this.state.isSending)
        {
            e.preventDefault()
            return false
        }
        this.setState({
            pendingMessage: e.currentTarget.value
        }, () => {
            const el = this.chatSendMsgRef.current!
            engine.autoResizeScrollableElement(el, {maxLines: 5})
        })
        return true
    }}
/>

For React onChange is like oninput in HTML5, so if you don't use React, then use the input event.


One of the answers uses rows attribute (instead of CSS's height as my code above does), here's an alternative implementation that doesn't use outside variables (BUT just like that answer there is a bug: because rows is temporaily set to 1, something bad happens with <html>'s scrollTop when you input AND <html> can be scrolled):

// @author Arzet Ro, 2021 <arzeth0@gmail.com>
// @license CC0 (Creative Commons Zero v1.0 Universal) (i.e. Public Domain)
// @source https://stackoverflow.com/a/70341077/332012
function autoResizeTextareaByChangingRows (
    el,
    {minLines, maxLines}
)
{
    const FN_NAME = 'autoResizeTextareaByChangingRows'
    if (
        typeof minLines !== 'undefined'
        && minLines !== null
        && Number.isNaN(+minLines)
    )
    {
        console.warn('%O:: minLines (%O) as a number is NaN', FN_NAME, minLines)
    }
    if (
        typeof maxLines !== 'undefined'
        && maxLines !== null
        && Number.isNaN(+maxLines)
    )
    {
        console.warn('%O:: maxLines (%O) as a number is NaN', FN_NAME, maxLines)
    }
    minLines = (
        minLines
        ? (
            Math.round(+minLines || 0) > 1
            ? Math.round(+minLines || 0)
            : 1
        )
        : 1
    )
    maxLines = (
        maxLines
        ? (Math.round(+maxLines || 0) || Infinity)
        : Infinity
    )
    el.setAttribute(
        'rows',
        '1',
    )
    const style = window.getComputedStyle(el)
    const unpreparedLineHeight = style.getPropertyValue('line-height')
    if (unpreparedLineHeight === 'normal')
    {
        console.error('%O:: line-height is unset for %O', FN_NAME, el)
    }
    const rows = Math.max(minLines, Math.min(maxLines,
        Math.round(
            (
                el.scrollHeight
                - parseFloat(style.getPropertyValue('padding-top'))
                - parseFloat(style.getPropertyValue('padding-bottom'))
            ) / (
                unpreparedLineHeight === 'normal'
                ? 1.15 * parseFloat(style.getPropertyValue('font-size')) // 1.15 is a wrong number
                : parseFloat(unpreparedLineHeight)
            )
        )
   ))
    el.setAttribute(
        'rows',
        rows.toString()
    )
}

const textarea = document.querySelector('textarea')
textarea.oninput = function ()
{
    autoResizeTextareaByChangingRows(textarea, {maxLines: 5})
}
Arzet Ro
  • 340
  • 1
  • 4
  • 10
-1

using

<div contentEditable></div>

may also do the same work, expanding it self, and requires no js

Guan Yuxin
  • 400
  • 5
  • 12