Textarea

.textarea is declared in the same rule as .input and .select — same border, radius, shadow, focus ring and 16px-on-touch — and then adds three declarations of its own. The interesting one is field-sizing: content, which makes it grow as the reader types, with no JavaScript.

When to use it

Put it on a real <textarea>, wrapped in a .field with a label. Use it whenever the answer might run to more than one line: a note, a description, a message, an address.

Type into it — the box grows to fit

Visible to everyone on the project.

<div class="field" style="max-inline-size:26rem"> <label class="label" for="ta-note">Note</label> <textarea class="textarea" id="ta-note" placeholder="Type a few lines and watch it grow"></textarea> <p class="help">Visible to everyone on the project.</p> </div>
<div class="field" style="max-inline-size:26rem">
  <label class="label" for="ta-note">Note</label>
  <textarea class="textarea" id="ta-note" placeholder="Type a few lines and watch it grow"></textarea>
  <p class="help">Visible to everyone on the project.</p>
</div>

The three declarations

field-sizing: content

The browser sizes the control to its content instead of to the rows attribute. Every implementation of "auto-growing textarea" before this was JavaScript measuring a hidden clone on every keystroke; this is one declaration and it costs nothing.

It is not in every browser yet. Where it is missing the textarea is simply its minimum height and scrolls — the ordinary behaviour. The feature degrades to the default rather than breaking, which is why Deck ships it unguarded rather than behind an @supports.

A two-row floor

min-block-size: calc(var(--control-h) * 2) — twice the height of an input. An empty auto-sizing textarea would otherwise be one row tall and indistinguishable from a text input, which is the one thing a textarea has to communicate before it is used.

A 22rem ceiling

max-block-size: 22rem. Without it, a textarea with a thousand words in it grows to a thousand words tall and the reader loses both the rest of the form and the submit button. At the ceiling it starts scrolling, which is the ordinary behaviour again.

Vertical resize stays available

resize: vertical rather than none or both. Vertical because a reader who wants more room should have it — the ceiling is Deck's guess, not theirs. Not horizontal, because a textarea wider than its .field breaks the form layout around it.

Past the ceiling it scrolls; the corner still resizes
<div class="field" style="max-inline-size:26rem"> <label class="label" for="ta-long">Release notes</label> <textarea class="textarea" id="ta-long">Deck 0.1.0 The first tagged build. One stylesheet, no build step, no runtime dependencies. Paste enough text in here and the box stops growing at 22rem and starts scrolling. Drag the corner if you want more room than that.</textarea> </div>
<div class="field" style="max-inline-size:26rem">
  <label class="label" for="ta-long">Release notes</label>
  <textarea class="textarea" id="ta-long">Deck 0.1.0

The first tagged build. One stylesheet, no build step, no runtime dependencies.

Paste enough text in here and the box stops growing at 22rem and starts scrolling. Drag the corner if you want more room than that.</textarea>
</div>

States

Every state comes from the shared rule, so a textarea hovers, focuses, disables and validates exactly as an input does. :user-invalid rather than :invalid means an empty required textarea is not marked wrong before the reader has touched it — the reasoning is on the input page.

<div class="stack stack-3" style="max-inline-size:26rem"> <textarea class="textarea" aria-label="Ordinary">Ordinary</textarea> <textarea class="textarea" readonly aria-label="Readonly">Readonly</textarea> <textarea class="textarea" disabled aria-label="Disabled">Disabled</textarea> </div>
<div class="stack stack-3" style="max-inline-size:26rem">
  <textarea class="textarea" aria-label="Ordinary">Ordinary</textarea>
  <textarea class="textarea" readonly aria-label="Readonly">Readonly</textarea>
  <textarea class="textarea" disabled aria-label="Disabled">Disabled</textarea>
</div>

Counting characters

Deck ships no character counter for .textarea. The .editor component has one — .editor-count, with an .is-over state driven by data-limit — but that is a rich text editor rather than a plain control.

For a plain textarea, an <output> beside it is the accessible version, because <output> is an implicit live region and the remaining count is announced as it changes.

<div class="field">
  <label class="label" for="bio">Bio</label>
  <textarea class="textarea" id="bio" maxlength="280"></textarea>
  <output class="help" for="bio">280 left</output>
</div>

Classes

.textarea is declared alongside .input and .select in one shared rule, so the extractor does not treat it as a component root and there is no completeness check on this page.

Classes, generated from the stylesheet source
Class Layer What it does Source
.textarea deck.components Grows with its content instead of scrolling in a 4-row box 06-forms.css:49

Tokens

--control-h is doing double duty here: the same token that sets an input's height sets a textarea's minimum, so the two line up in a form.

Design tokens this component reads
Token Live Declared value
--control-h 42px
--surface light-dark(var(--ink-0), var(--ink-950))
--surface-2 light-dark(var(--ink-25), var(--ink-900))
--bg-sunken light-dark(var(--ink-100), oklch(8.6% .012 var(--hue-neutral)))
--line-strong light-dark(var(--ink-300), oklch(36% .018 var(--hue-neutral)))
--focus light-dark(var(--brand-500), var(--brand-300))
--ring 0 0 0 3px light-dark(oklch(from var(--focus) l c h / .32), oklch(from var(--focus) l c h / .42))
--r-sm 7px
--shadow-1 0 1px 2px oklch(20% .03 var(--shadow-color) / .06), 0 1px 1px oklch(20% .03 var(--shadow-color) / .04)
--text-base .9375rem
--text-faint light-dark(var(--ink-500), var(--ink-500))
--bad-500 oklch(58% calc(var(--chroma-status) * 1.25) var(--hue-bad))
--space-2 .5rem
--space-3 .75rem
--dur-1 110ms

Accessibility

  • It needs a label like any other control. A <label class="label" for="id"> is the right answer; aria-label is the fallback the short examples on this page use.
  • Growing as you type is announced as nothing, which is correct — it is a visual convenience. But it does move everything below the textarea down the page while the reader is typing, and for a reader using a screen magnifier that can be disorienting. The 22rem ceiling bounds how bad that gets.
  • 16px on touch is the same iOS fix as on an input. Below 16px, Safari zooms the page when the control takes focus, and a page that zooms unexpectedly loses a low-vision reader's place.
  • resize: vertical is a genuine accessibility feature. Removing the resize handle takes away a reader's ability to see more of their own text at once. Deck constrains the axis rather than removing the affordance.
  • A maxlength with no visible counter is a trap. The control silently stops accepting input, which reads as a broken keyboard. If you set a limit, show the remaining count — and put it in an <output> so it is announced.
  • Placeholder text is not a label and is deliberately --text-faint, which is below 4.5:1. It is a hint, and Deck styles it as one.

Right to left

Padding is logical and the browser handles the text direction itself, so a textarea mirrors with no extra rules. The resize handle moves to the opposite corner on its own, because the browser draws it at the end edge.

<div dir="rtl" class="field" style="max-inline-size:26rem"> <label class="label" for="ta-rtl">ملاحظة</label> <textarea class="textarea" id="ta-rtl" placeholder="اكتب هنا"></textarea> </div>
<div dir="rtl" class="field" style="max-inline-size:26rem">
  <label class="label" for="ta-rtl">ملاحظة</label>
  <textarea class="textarea" id="ta-rtl" placeholder="اكتب هنا"></textarea>
</div>

Reduced motion

The border, shadow and background transition over 110ms, and the global reset collapses all three to .01ms under prefers-reduced-motion: reduce.

The growth from field-sizing is not a transition — the box jumps to its new height on each new line. That is deliberate: an animated height on every keystroke would be motion nobody asked for, and it would lag the caret.

Printing

src/99-print.css gives every control a plain #999 border on white and drops the minimum height, so a printed form reads as a filled-in document rather than a screenshot of a web page.

.textarea then gets a line of its own:

.textarea { max-block-size: none !important; block-size: auto !important; }

Both halves matter. Clearing max-block-size undoes the 22rem ceiling, and block-size: auto lets the box take its content's height — so a long note prints in full instead of printing the first 22rem of itself with no indication that anything was cut.

Overriding it

/* One textarea, no layer needed */
<textarea class="textarea" style="max-block-size:40rem"></textarea>

@layer app.components {
  /* A taller floor for a control that is mostly used for long text */
  .textarea { min-block-size: calc(var(--control-h) * 4); }

  /* Turn off the growth and go back to rows */
  .textarea-fixed { field-sizing: fixed; }
}

field-sizing: fixed is the explicit way back to the old behaviour, and it makes the rows attribute meaningful again.

When not to use it

  • Not for a single line. Use .input. A textarea accepts newlines, which means a name field can contain one and your validation now has to care.
  • Not for formatted text. A textarea holds plain text. If the reader needs bold, lists or links, use .editor, which is a contenteditable region with a toolbar and a hidden input that carries the HTML.
  • Not for code. A textarea has no monospace font, no tab handling and no line numbers. Add .mono at minimum, and consider whether an editor component is what you actually need.
  • Not for a value with a known shape. A date, a phone number or a quantity has a control of its own — .datefield, .phone, .number — each of which gives a phone the right keypad and validates without you writing anything.
  • Not without a ceiling, if you raise it. Removing max-block-size lets a long value push the submit button below the fold indefinitely. If you raise it, raise it to a number rather than to none.