Textarea Auto-grow
The text area grows line by line up to 200 px, then scrolls; an "n / 200" counter turns amber at 150 and red at the limit.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Forms has 20 effects, including 2 free. Explore the category →
3 usage examples



How it works
On every input event, the script resets the height to auto, reads scrollHeight, adds the border thickness and applies the result, capped by the field's computed max-height (200 px by default). While the text fits there is no scrollbar; beyond the cap, overflow-y switches to auto and the area scrolls. A ResizeObserver recomputes when the width changes (phone rotation, narrowing column).
The counter shows the value's length over the maximum read from the maxlength attribute (200 shipped); the number after the slash is rewritten by the script, so it follows your attribute. At 75% (150 of 200) it gets the is-warning class, amber; at the limit, is-limit, red.
The <textarea> tag is empty in the shipped HTML: the placeholder shows and the counter starts at 0. The script is wrapped in a function: pasted twice, or next to an existing textarea variable, it throws no error, and a root already set up (data-agrw-ready) is not wired again.
Accessibility
- Label: "Message" in a visually hidden
<label>; rename it to fit your form ("Your reply", "Description"). - Linked counter: the area points to the counter through
aria-describedby, read when it is reached with the keyboard. - Thresholds announced, not every key: an
aria-live="polite"region says "50 characters left" when entering amber, then "200-character limit reached"; nothing is read in between. - Motion: the height changes without animation, nothing to switch off under
prefers-reduced-motion. - Limit: beyond 200 px the area scrolls; for very long text, a fixed height shown from the start remains more comfortable.
Browser compatibility
2.7 KB of JavaScript, CSS with variables. Measured on 08/11/2026 in Chromium, Firefox and WebKit (Playwright): empty initial value, visible placeholder, "1 / 200" after one key, height following the lines then stopping at 200 px with scrolling, paste cut at 200, two independent copies, form reset, no errors. ResizeObserver is the most recent feature (Chrome 64, Firefox 69, Safari 13.1, according to @mdn/browser-compat-data).
Without ResizeObserver (Chrome 51-63, Safari 10-13), the area grows while typing but is not recomputed when only its width changes. Without JavaScript: fixed two-line area, counter stuck at 0, maxlength still enforced by the browser.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0289-root">
<div class="agrw-group">
<label class="agrw-field">
<span class="agrw-sr">Message</span>
<textarea class="agrw-input" name="message" placeholder="Type your message..." maxlength="200"></textarea>
</label>
<div class="agrw-count"><span class="agrw-current">0</span> / <span class="agrw-max">200</span></div>
<span class="agrw-sr agrw-status" aria-live="polite"></span>
<span class="agrw-words" hidden>
<span data-msg="warning">{n} characters left</span>
<span data-msg="limit">{max}-character limit reached</span>
</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Character limit (HTML — maxlength on the area) |
200 | The counter and its thresholds follow: with 500, amber arrives at 375. The number after the slash is rewritten by the script. |
Heights (CSS — --agrw-min-height, --agrw-max-height on .fx-0289-root) |
60px · 200px | Starting height and cap before scrolling. The script reads the computed cap, nothing to change in the JS. |
Warning threshold (JS — Math.ceil(max * 0.75)) |
75% of the maximum | Switch to 0.9 to warn only near the limit. |
Counter colours (CSS — --agrw-muted, --agrw-warning, --agrw-limit) |
#8b8b93 · #f59e0b · #ef4444 | On a light background, prefer #c2410c and #dc2626 for enough contrast. |
Field (CSS — --agrw-field, --agrw-border, --agrw-focus, --agrw-focus-field, --agrw-text) |
5% white · 10% white · #6366f1 · 5% indigo · #fff | Background, border, focus colour and background, typed text. |
Width and frame (CSS — --agrw-width, --agrw-bg, --agrw-root-min-height) |
280px · #0a0a0f · 300px | On a real page: --agrw-width:100%; --agrw-bg:transparent; --agrw-root-min-height:0. |
Announced messages (HTML — .agrw-words) |
"{n} characters left" · "{max}-character limit reached" | {n} becomes the number left, {max} the limit. Translate them here. |
FAQ
reset. If you change the value from a script, then fire textarea.dispatchEvent(new Event('input', {bubbles: true})).<textarea> and </textarea> (a line break, spaces): it becomes the starting value, hides the placeholder and counts towards the limit. Keep both tags touching when you edit the HTML..agrw-count from the HTML stops the script from setting up, as it needs it. To keep only the growing, leave the element and hide it in CSS (.fx-0289-root .agrw-count{display:none}): the field still points to it through aria-describedby, so screen readers keep hearing the character count, which stays useful.