Reputation: 25377
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.
Upvotes: 37
Views: 47494
Reputation: 109
Split line to array with newline charaters and set it's length to rows
:
textarea.addEventListener('input',
e => e.target.rows = e.target.value.split('\n').length
);
Upvotes: 1
Reputation: 1810
For those using Angular and having the same issue, use
<textarea cdkTextareaAutosize formControlName="description" name="description" matInput placeholder="Description"></textarea>
The key here is cdkTextareaAutosize
which will automatically resize the textarea to fit its content. Read more here.
I hope this helps someone.
Upvotes: 0
Reputation: 1283
This will handle pasting, deleting, text-wrapping, manual returns, etc. & accounts for padding and box-sizing issues.
scrollHeight
.overflow
to hidden
and hard locks the current computed width (minus left/right border width and left/right padding widths), then forces box-sizing
to content-box
to get an accurate line-height
and scrollHeight
reading. border-width
and padding-inline
is also removed to keep the textarea width consistent when switching box-sizing
. This all helps keep the math accurate when dealing with text wrapping.line-height
and top/bottom-padding
pixel values.scrollHeight
pixel value (rounded since chrome rounds
and we're hoping to handle all browsers consistently).overflow
, box-sizing
, width
, padding-inline
and border-width
overrides.block_padding
from scroll_height
then divides that by the line_height
to get the needed rows
. The rows
value is rounded to the nearest integer since it will always be within ~.1 of the correct whole number.rows
value is applied as the
rows
attribute unless the row_limit
is smaller, then the row_limit
is used instead.I removed the loop code that was used figure out the row count because I was able to verify the math on the division formula works out within about .1 of the row count needed. Therefore a simple Math.round()
ensures the row count is accurate. I was not able to break this in testing so if it turns out to be wrong please feel free to suggest a tweak.
I also ran into issues when line-height
is not explicitly set on the text area as in that case the computed value for line-height
comes back as "normal"
and not the actual computed value. This new version accounts for this eventuality and handles it properly as well.
I did not set the textarea
to position: absolute;
while swapping it's box-sizing
out as I did not notice a need for it in my testing. It's worth mentioning because I suppose there could be a scenario where these minor changes might cause a layout shift depending on how the page is styled and if that happens you could add add that and then remove it along with the box-sizing override and removal.
(you only need the one JS function, everything else is just for the demo)
function autosize(textarea_id, row_limit) {
// Set default for row_limit parameter
row_limit = parseInt(row_limit ?? '5');
if (!row_limit) {
row_limit = 5;
}
// Get the element
const textarea = document.getElementById(textarea_id);
// Set required styles for this to function properly.
textarea.style.setProperty('resize', 'none');
textarea.style.setProperty('min-height', '0');
textarea.style.setProperty('max-height', 'none');
textarea.style.setProperty('height', 'auto');
// Set rows attribute to number of lines in content
textarea.oninput = function() {
// Reset rows attribute to get accurate scrollHeight
textarea.setAttribute('rows', '1');
// Get the computed values object reference
const cs = getComputedStyle(textarea);
// Force content-box for size accurate line-height calculation
// Remove scrollbars, lock width (subtract inline padding and inline border widths)
// and remove inline padding and borders to keep width consistent (for text wrapping accuracy)
const inline_padding = parseFloat(cs['padding-left']) + parseFloat(cs['padding-right']);
const inline_border_width = parseFloat(cs['border-left-width']) + parseFloat(cs['border-right-width']);
textarea.style.setProperty('overflow', 'hidden', 'important');
textarea.style.setProperty('width', (parseFloat(cs['width']) - inline_padding - inline_border_width) + 'px');
textarea.style.setProperty('box-sizing', 'content-box');
textarea.style.setProperty('padding-inline', '0');
textarea.style.setProperty('border-width', '0');
// Get the base line height, and top / bottom padding.
const block_padding = parseFloat(cs['padding-top']) + parseFloat(cs['padding-bottom']);
const line_height =
// If line-height is not explicitly set, use the computed height value (ignore padding due to content-box)
cs['line-height'] === 'normal' ? parseFloat(cs['height'])
// Otherwise (line-height is explicitly set), use the computed line-height value.
: parseFloat(cs['line-height']);
// Get the scroll height (rounding to be safe to ensure cross browser consistency)
const scroll_height = Math.round(textarea.scrollHeight);
// Undo overflow, width, border-width, box-sizing & inline padding overrides
textarea.style.removeProperty('width');
textarea.style.removeProperty('box-sizing');
textarea.style.removeProperty('padding-inline');
textarea.style.removeProperty('border-width');
textarea.style.removeProperty('overflow');
// Subtract block_padding from scroll_height and divide that by our line_height to get the row count.
// Round to nearest integer as it will always be within ~.1 of the correct whole number.
const rows = Math.round((scroll_height - block_padding) / line_height);
// Set the calculated rows attribute (limited by row_limit)
textarea.setAttribute("rows", "" + Math.min(rows, row_limit));
};
// Trigger the event to set the initial rows value
textarea.dispatchEvent(new Event('input', {
bubbles: true
}));
}
autosize('textarea');
* {
box-sizing: border-box;
}
textarea {
width: 100%;
max-width: 30rem;
font-family: sans-serif;
font-size: 1rem;
line-height: 1.5rem;
padding: .375rem;
}
<body>
<textarea id="textarea" placeholder="enter some text here :)"></textarea>
</body>
Upvotes: 4
Reputation: 517
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 <[email protected]>
// @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 <[email protected]>
// @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})
}
Upvotes: 0
Reputation: 1415
...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";
};
Upvotes: 2
Reputation: 151
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");
});
Upvotes: 3
Reputation: 430
using
<div contentEditable></div>
may also do the same work, expanding it self, and requires no js
Upvotes: 1
Reputation: 12855
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/
Upvotes: 11
Reputation: 349262
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.
Upvotes: 76