Textarea Autosize
stimeo--textarea-autosize
Grows a textarea to fit its content, clamped between minRows and maxRows.
The stimeo--textarea-autosize controller grows a textarea to fit its content. On connect and on every input it collapses the element (height:auto), reads scrollHeight, and sets an explicit pixel height clamped between minRows and maxRows (in line-height units). At the maxRows cap it switches to internal scrolling and sets the data-at-max-rows hook, and it exposes the approximate row count on the --stimeo-textarea-rows custom property. Height-only changes keep focus and caret. Behavior only — the height is written to the element's own inline style (never a CSS class); connect re-measures after a Turbo navigation, and the input listener is removed on disconnect. It dispatches stimeo--textarea-autosize:resize when the height changes.
<%# Textarea autosize demo: type and the field grows to fit, up to maxRows (then it
scrolls). The controller sets the element's inline height from scrollHeight and
toggles data-at-max-rows at the cap; it listens for input itself, so no consumer
JS is needed. This demo only sets the textarea's base look. %>
<div class="textarea-autosize-demo">
<label class="textarea-autosize-demo__label" for="ta-autosize">
<%= t("components.textarea_autosize.demo.label") %>
</label>
<textarea
id="ta-autosize"
class="textarea-autosize-demo__field"
rows="2"
placeholder="<%= t('components.textarea_autosize.demo.placeholder') %>"
data-controller="stimeo--textarea-autosize"
data-stimeo--textarea-autosize-max-rows-value="8"></textarea>
</div>
/*
* Presentation-only styles for the textarea autosize demo.
* The library owns the textarea's height (set inline from scrollHeight) and toggles
* data-at-max-rows; this CSS only provides the base look and the line metrics the
* controller measures against. resize:none because the library manages height.
*/
.textarea-autosize-demo {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 32rem;
}
.textarea-autosize-demo__label {
font-weight: 600;
color: var(--fg);
}
.textarea-autosize-demo__field {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font: inherit;
line-height: 1.5;
resize: none;
overflow: hidden;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--textarea-autosize"
Values
| Name | Description | Attribute |
|---|---|---|
minRows
|
Minimum number of rows (default 1). | data-stimeo--textarea-autosize-min-rows-value |
maxRows
|
Maximum number of rows; 0 means unlimited (default 0). | data-stimeo--textarea-autosize-max-rows-value |
Actions
| Name | Action |
|---|---|
resize
|
stimeo--textarea-autosize#resize |
Events
| Name | Description | Event |
|---|---|---|
resize
|
Fires when the height changes, with detail.height / detail.rows. |
stimeo--textarea-autosize:resize |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-at-max-rows |
Textarea | Present once the content reaches maxRows (the field then scrolls). |
--stimeo-textarea-rows |
Textarea | The current approximate row count. |