Overview & Philosophy
ln-autoresize is applied directly to multi-line input fields via <textarea data-ln-autoresize rows="1">. It listens for native browser input events: on each keystroke, it resets height = 'auto', measures scrollHeight, and immediately assigns the calculated pixel height to the style attribute. It maintains zero internal state and introduces no heavy observers, executing a clean, synchronous layout calculation that smoothly expands and collapses textareas without visual flicker.
In ln-ashlar, this is solved by an ultra-lightweight UX helper primitive (~47 lines of JavaScript) hooking directly into the browser's native layout cycle.
The Scroll-Height Observation Trick
To shrink a textarea successfully, its CSS height must be reset to auto before reading scrollHeight. Otherwise, the dynamic container stays locked to its previous large size and never collapses. Resetting to auto and reading scrollHeight in immediate sequence forces a synchronous layout flush in the browser. It resizes perfectly with zero visual flicker.
Concerns Scoped Strictly by Layer
The growing input experience is achieved by decoupling layout ceilings, browser defaults, and active observation:
-
The Content Shell (HTML
rowsAttribute): Standard inputs default torows="2", causing empty textareas to start tall and snaps shut on initialization. Explicitly declaringrows="1"aligns the starting layout with the initial javascript paint for a seamless loading experience. -
The Ceiling Controller (CSS Constraints): Limits on height are defined strictly inside project stylesheets using standard
max-height: 6remandoverflow-y: auto. This lets developers cap expansion and enable scrolling using pure CSS, without touching a single line of JavaScript. -
The Resizing Primitive (
ln-autoresize): The script acts as a stateless, reactive observation node. It binds only to theinputevent stream, tracks pre-filled database text on load, and responds to programmatic changes.
For the full guide see the ln-autoresize README and the architecture reference.
Anatomy
The complete invocation. There is no markup contract beyond this:
<textarea data-ln-autoresize rows="1" placeholder="Notes..."></textarea>
Element type — <textarea> only
The component validates tagName === 'TEXTAREA' in the constructor and bails on anything else with a console warning ([ln-autoresize] Can only be applied to <textarea>, got: …). For contenteditable auto-grow, this is a different problem — those elements already grow with content because they are not constrained by rows.
rows="1" — initial height before JS runs
The browser default for <textarea> is rows="2", which means the field renders at two lines before ln-autoresize attaches and runs the initial _resize. Setting rows="1" makes the empty starting state match the post-init state — no visible snap-shut on first paint. rows is not a "max rows" cap. Once ln-autoresize writes inline style.height, that wins over rows for the lifetime of the instance.
Pairing with CSS for capped growth
Pure auto-resize grows forever. Most real use cases want a ceiling past which the field starts scrolling instead. The component does nothing here — that is CSS:
textarea[data-ln-autoresize] {
resize: none;
max-height: 6rem;
overflow-y: auto;
}
resize: none removes the manual drag handle (the global @mixin form-textarea sets resize: vertical by default — when you opt into autoresize, override it, since the manual handle and auto-grow are the same affordance done two different ways and they fight each other). max-height caps the inline height the component writes; overflow-y: auto reveals the scrollbar past the cap.
What state goes where
"Should this textarea auto-size?" lives on the presence of data-ln-autoresize (owned by ln-autoresize). "Current height" lives on the inline style.height the component writes after every input and on construction. "Maximum height" and "minimum height" live in CSS, owned by the project. "Initial height before JS" lives on the rows attribute, owned by the browser. The component's instance has two fields: dom and _onInput. Nothing else.
Attributes reference
ln-autoresize's entire attribute surface is one marker on one element type. Every other attribute (rows, placeholder, id, name, required, maxlength) is a native textarea attribute that the component does not read or write.
| Attribute | On element | Owned by | Description |
|---|---|---|---|
data-ln-autoresize | <textarea> | ln-autoresize | Marks the textarea as auto-sizing. Presence creates the instance and runs an initial _resize. No value. |
rows | <textarea> | Browser | Initial visible row count before JS runs. Use "1" to avoid a snap-shut on first paint. |
Example — Basic auto-grow
Type into the field. The height tracks the content as you add and remove lines. Open DevTools and watch style="height: …px" update on each keystroke — that is the only thing the component writes.
Example — Pre-filled content
Server-rendered content sizes to fit on first paint. The constructor calls _resize once before returning, so by the time the page is interactive the height already matches the value — there is no flash of the default rows-height before the resize catches up.
Example — Capped at 6rem with scroll
Pair data-ln-autoresize with CSS max-height and overflow-y: auto to cap growth and enable scroll. The <style> block at the top of this page scopes the rule to #demo-capped-wrapper textarea.
Example — Programmatic value writes need input dispatch
Setting el.value = … directly is silent — the platform does not fire input, so ln-autoresize's listener never runs. Either dispatch the event yourself, or go through [data-ln-form]'s ln-form:fill event which dispatches input for every populated field automatically. The two buttons below illustrate the difference: the first sets .value silently and the height stays wrong; the second dispatches input and the height adjusts.
Example — Reset inside [data-ln-form] re-measures automatically
Type a long reply, then click Reset (lnForm). The textarea clears AND shrinks back to one row in the same operation. lnForm.reset() dispatches synthetic input on every field after the native reset, which ln-autoresize catches and uses to re-measure. No coordinator workaround needed.
For comparison, the second button is a bare <button type="reset"> — it fires the platform reset event but not through the lnForm API path. Native reset on its own does NOT dispatch input, so the textarea clears but the height stays tall. This is the case you must work around manually if you cannot use the lnForm.reset() API — see the autoresize README "Common mistakes" item 4.
Example — Dynamically inserted textareas
Click the button to create a new <textarea data-ln-autoresize> via JavaScript. The shared MutationObserver registered through registerComponent picks it up automatically — no manual lnAutoresize(container) call is needed for live document inserts.
Example — Hidden-then-revealed needs manual _resize
A textarea inside display: none measures scrollHeight as 0 at construction time. When the parent becomes visible later, the textarea will be too short. The component does not detect visibility changes — there is no IntersectionObserver. Force a re-measure when the panel opens.
API
const el = document.querySelector('[data-ln-autoresize]');
el.lnAutoresize._resize(); // force a re-measure (after .value writes,
// reveal of hidden parent, font load, etc.)
el.lnAutoresize.destroy(); // remove input listener, clear inline height
// (data-ln-autoresize attribute stays in place)
window.lnAutoresize(root); // re-run init scan over `root` — only needed
// for Shadow DOM / iframe roots that the
// global MutationObserver cannot see
Common mistakes
1. Setting .value directly and expecting the height to update
Silent — the platform does not fire input. Dispatch it yourself, or use [data-ln-form]'s ln-form:fill event.
2. Using rows as a max-rows cap
rows is the initial render height before JS attaches. Once the inline style.height is written, rows is overridden. Use CSS max-height to cap.
3. Mounting inside a hidden parent
Construction-time scrollHeight is 0 when the element is hidden. Call _resize after reveal.
4. Bare <button type="reset"> outside [data-ln-form] leaves the height tall
Native reset does not fire input. Either wrap the form in data-ln-form and call form.lnForm.reset(), or dispatch input manually in a setTimeout after the native reset event fires.
5. Applying to non-textarea elements
Logged warning + the attribute does nothing. Use a real <textarea>.
6. Forgetting to override resize: vertical
The default @mixin form-textarea ships resize: vertical, giving a manual drag handle. The handle works until the user types; then _resize overwrites their dragged height. Set resize: none on the selector when you opt into autoresize.