Auto-growing Textarea
A text box that starts one line tall and grows smoothly as you add lines, easing to its new height rather than jumping or showing a scrollbar. Delete lines and it shrinks back. Past a maximum it stops growing and scrolls instead.
Press Enter for a new line.
How to use it
- Paste the snippet just before
</head>. - Wrap a textarea in an element with
data-auto-growing-textarea. - Set the smallest size with the textarea’s
rowsattribute and style it as you like.
Options
Add any of these attributes to the same element.
data-max-rows | The most lines it grows to before scrolling. Default 8. |
data-duration | How long each change of height takes. Default 220 ms. |
data-autoplay | It types a short message line by line and deletes it while in view, and stops as soon as you use the field. |
data-text | What autoplay types. A | starts a new line. |
It measures with a hidden copy of the textarea, so it copes with wrapping, pasting and resizing the window.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Auto-growing Textarea: the text box grows smoothly as you add lines and
// shrinks back as you remove them, instead of growing a scrollbar. Past
// the maximum it stops growing and scrolls.
// Usage: <div data-auto-growing-textarea><textarea rows="1"></textarea></div>
// Style the textarea however you like; its rows attribute is the
// smallest it gets.
// Optional: data-max-rows (the tallest it grows before scrolling, default
// 8), data-duration (ms, default 220), data-autoplay (it types a short
// message line by line and clears it, over and over while in view; it
// stops as soon as you use the field), data-text (what autoplay types;
// "|" starts a new line).
var DEFAULTS = { maxRows: 8, duration: 220, text: 'Hi Sam,|Lunch on Friday?|There’s a new place by the river.|See you there!' };
var css =
'.agt textarea{resize:none;overflow-y:hidden;box-sizing:border-box}' +
'.agt-scroll textarea{overflow-y:auto}' +
'.agt-mirror{position:absolute!important;visibility:hidden!important;pointer-events:none;left:0;top:0;height:auto!important;min-height:0!important;max-height:none!important;overflow:hidden!important;z-index:-1}' +
'.agt [data-field]{box-sizing:border-box;overflow:hidden}' +
'.agt-inner{display:block;white-space:pre-wrap;word-wrap:break-word}' +
'.agt-caret{display:inline-block;width:1.5px;height:1.1em;margin-left:1px;vertical-align:-.15em;background:currentColor;animation:agt-blink 1s steps(1) infinite}' +
'@keyframes agt-blink{50%{opacity:0}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
var d = el.dataset;
var ta = el.querySelector('textarea');
var field = ta || el.querySelector('[data-field]');
if (!field) return;
var dur = parseFloat(d.duration) || DEFAULTS.duration;
var maxRows = parseInt(d.maxRows, 10) || DEFAULTS.maxRows;
el.classList.add('agt');
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var mirror = null, inner = null, caret = null;
if (ta) {
// A hidden twin with height:auto tells us the height the text needs.
mirror = ta.cloneNode(false);
mirror.removeAttribute('id');
mirror.removeAttribute('name');
mirror.setAttribute('aria-hidden', 'true');
mirror.tabIndex = -1;
mirror.className = (ta.className + ' agt-mirror').trim();
el.appendChild(mirror);
} else {
inner = document.createElement('span');
inner.className = 'agt-inner';
var txt = document.createElement('span');
caret = document.createElement('span');
caret.className = 'agt-caret';
inner.appendChild(txt);
inner.appendChild(caret);
field.textContent = '';
field.appendChild(inner);
inner = txt;
}
function px(v) { return parseFloat(v) || 0; }
function needed() {
var cs = getComputedStyle(field);
var lh = px(cs.lineHeight) || px(cs.fontSize) * 1.4;
var chrome = px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
var rows = ta ? (parseInt(ta.getAttribute('rows'), 10) || 1) : 1;
var h;
if (ta) {
mirror.style.width = ta.offsetWidth + 'px';
mirror.value = ta.value || ' ';
mirror.rows = rows;
h = mirror.scrollHeight + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
} else {
h = inner.parentNode.offsetHeight + chrome;
}
var min = rows * lh + chrome, max = maxRows * lh + chrome;
el.classList.toggle('agt-scroll', h > max + 1);
return Math.round(Math.min(max, Math.max(min, h)));
}
var target = 0, anim = null;
function fit(instant) {
var h = needed();
if (h === target) return;
target = h;
if (anim) anim.pause();
if (instant || reduced()) { field.style.height = h + 'px'; return; }
anim = anime.animate(field, { height: h, duration: dur, ease: 'out(3)' });
}
fit(true);
if (window.ResizeObserver) {
var lastW = el.clientWidth;
new ResizeObserver(function () { if (el.clientWidth !== lastW) { lastW = el.clientWidth; target = 0; fit(true); } }).observe(el);
}
if (ta) {
ta.addEventListener('input', function (e) { if (e.isTrusted) stop(); fit(); });
ta.addEventListener('pointerdown', stop);
ta.addEventListener('focus', stop);
}
function set(v) {
if (ta) ta.value = v; else inner.textContent = v;
fit();
if (!ta) field.scrollTop = field.scrollHeight;
}
// Autoplay: type a message one character at a time, then clear it.
var timer = 0, stopped = false, auto = on(d.autoplay) && !reduced();
function stop() {
if (stopped) return;
stopped = true;
clearTimeout(timer);
if (auto && ta && document.activeElement === ta && ta.value === typed) set('');
}
var msg = (d.text || DEFAULTS.text).split('|').join('\n'), typed = '';
var steps = [];
for (var i = 1; i <= msg.length; i++) {
(function (n) {
steps.push([function () { typed = msg.slice(0, n); set(typed); }, msg[n - 1] === '\n' ? 420 : 45 + Math.random() * 70]);
})(i);
}
steps.push([function () {}, 1400]);
// Delete line by line so it shrinks a step at a time.
var lines = msg.split('\n');
for (var k = lines.length - 1; k >= 0; k--) {
(function (n) { steps.push([function () { typed = lines.slice(0, n).join('\n'); set(typed); }, 380]); })(k);
}
steps.push([function () {}, 700]);
function run(i) {
if (stopped) return;
steps[i][0]();
timer = setTimeout(function () { run((i + 1) % steps.length); }, steps[i][1]);
}
if (auto) {
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (stopped) return;
if (entries[0].isIntersecting) { set(''); timer = setTimeout(function () { run(0); }, 600); }
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-auto-growing-textarea]').forEach(function (el) {
if (el._agt) return;
el._agt = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-auto-growing-textarea data-max-rows="6">
<textarea rows="1" placeholder="Write a message…" aria-label="Message"></textarea>
</div>