shshadcn-htmx

Components

Autosize Textarea

A real <textarea> that grows and shrinks to fit its content between min/max bounds — delivered by the single CSS line field-sizing: content instead of the classic scrollHeight JavaScript hack. Where unsupported it degrades to a plain fixed field. Zero JS.

Installation

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/autosize-textarea.json

2. Use it

components/ui/autosize-textarea.tsx
import { AutosizeTextarea } from "@/components/ui/autosize-textarea"

<AutosizeTextarea name="reply" placeholder="Write a reply…" />
Or copy the source manually
components/ui/autosize-textarea.tsx
/** @jsxImportSource hono/jsx */
import { cn, type ClassValue } from "@/registry/lib/cn"

// Autosize Textarea — a <textarea> that grows and shrinks to fit its content
// between min/max bounds, delivered by ONE CSS declaration instead of the
// classic scrollHeight JS hack. Source of truth for the platform behaviour:
//   repos/mdn/files/en-us/web/css/reference/properties/field-sizing/index.md
//     - "field-sizing: content overrides the default preferred sizing of form
//        elements … configure text inputs to shrinkwrap their content and grow
//        as more text is entered."
//     - "<textarea> … If unable to grow due to a width constraint, they grow in
//        height to display additional rows … When a height constraint is then
//        reached, they show a scrollbar."
//     - "rows/cols have no effect on <textarea> with field-sizing: content set."
//     - "using min-height and max-height alongside field-sizing: content is
//        quite effective … allow the control to grow and shrink … and prevent
//        the control from becoming too large or too small."
//   repos/mdn/files/en-us/web/html/reference/elements/textarea/index.md
//        (native element, dirname, wrap, readonly/disabled semantics)
//
// Tailwind v4 ships the utility natively — see
//   repos/tailwindcss/packages/tailwindcss/src/utilities.ts
//     staticUtility('field-sizing-content', [['field-sizing','content']])
//     staticUtility('field-sizing-fixed',   [['field-sizing','fixed']])
//
// htmx attrs (hx-post / hx-trigger="input changed delay:…") verified against
//   repos/htmx/www/reference.md (forwarded untouched via {...rest}).
//
// Style analogue: registry/ui/textarea.tsx (shares the base field styling).
//
// DEGRADATION: where field-sizing is unsupported, the rule is simply ignored
// and the element renders as an ordinary fixed-height textarea sized by
// min-height (and rows, which the browser then honours). No JS, no polyfill —
// progressive enhancement, not emulation. Pass autosize={false} to opt out
// explicitly (field-sizing-fixed), turning it into a plain bounded textarea.

// Bounds are expressed as utilities so a single CSS line drives the resize.
// Defaults: grow from ~2 lines up to ~10 lines, then scroll.
const base =
  "flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none " +
  "placeholder:text-muted-foreground " +
  "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " +
  "disabled:cursor-not-allowed disabled:opacity-50 " +
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " +
  "md:text-sm dark:bg-input/30 " +
  // htmx-request: dim while a request triggered by/targeting this textarea is
  // in flight (e.g. autosave / live-validation).
  "[&.htmx-request]:opacity-70"

// Sizing keyword maps. content => grows with text; fixed => classic textarea
// (resizable handle only). Kept as Record<Union,string> per house style.
const sizing: Record<"content" | "fixed", string> = {
  content: "field-sizing-content resize-none",
  fixed: "field-sizing-fixed resize-y",
}

export function autosizeTextareaClasses(opts?: {
  autosize?: boolean
  minHeight?: ClassValue
  maxHeight?: ClassValue
  class?: ClassValue
}): string {
  const auto = opts?.autosize !== false
  return cn(
    base,
    auto ? sizing.content : sizing.fixed,
    opts?.minHeight ?? "min-h-16",
    // Past the max, field-sizing: content yields a scrollbar (per MDN).
    opts?.maxHeight ?? "max-h-80",
    "overflow-auto",
    opts?.class,
  )
}

type AutosizeTextareaProps = {
  class?: ClassValue
  id?: string
  name?: string
  value?: string
  defaultValue?: string
  placeholder?: string
  required?: boolean
  disabled?: boolean
  readonly?: boolean

  // Autosize behaviour. true (default) => field-sizing: content; false =>
  // field-sizing: fixed, a plain bounded textarea with a drag handle.
  autosize?: boolean

  // Lower / upper growth bounds, as Tailwind height utilities. These are the
  // RIGHT levers for field-sizing per MDN — not width/height, which would
  // reimpose a fixed size and defeat the feature.
  minHeight?: ClassValue
  maxHeight?: ClassValue

  // rows/cols are honoured ONLY as the no-support fallback size — they have no
  // effect once field-sizing: content applies (MDN). Useful for graceful
  // degradation in older engines.
  rows?: number
  cols?: number

  // Validation
  minLength?: number
  maxLength?: number

  // Mobile UX
  autocomplete?: string
  autofocus?: boolean
  spellcheck?: boolean
  autocapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters"
  autocorrect?: "on" | "off"

  // Submits the text directionality (ltr/rtl) as a separate form field.
  // See repos/mdn/files/en-us/web/html/reference/elements/textarea/index.md
  dirname?: string

  // Wrapping
  wrap?: "hard" | "soft" | "off"

  // ARIA
  ariaLabel?: string
  ariaLabelledby?: string
  ariaDescribedby?: string
  ariaInvalid?: boolean | "grammar" | "spelling"
  ariaRequired?: boolean

  // Form metadata
  form?: string

  // htmx attributes — fire on blur or hx-trigger="input changed delay:300ms"
  // for live validation / autosave patterns. See repos/htmx/www/reference.md.
  "hx-get"?: string
  "hx-post"?: string
  "hx-put"?: string
  "hx-patch"?: string
  "hx-target"?: string
  "hx-swap"?: string
  "hx-trigger"?: string
  "hx-indicator"?: string
  "hx-vals"?: string
  "hx-include"?: string
}

export function AutosizeTextarea(props: AutosizeTextareaProps) {
  const {
    class: className,
    autosize,
    minHeight,
    maxHeight,
    value,
    defaultValue,
    ariaLabel,
    ariaLabelledby,
    ariaDescribedby,
    ariaInvalid,
    ariaRequired,
    ...rest
  } = props

  return (
    <textarea
      class={autosizeTextareaClasses({ autosize, minHeight, maxHeight, class: className })}
      aria-label={ariaLabel}
      aria-labelledby={ariaLabelledby}
      aria-describedby={ariaDescribedby}
      aria-invalid={ariaInvalid === undefined ? undefined : String(ariaInvalid)}
      aria-required={ariaRequired === undefined ? undefined : String(ariaRequired)}
      data-slot="autosize-textarea"
      data-autosize={autosize === false ? "false" : "true"}
      {...rest}
    >{value ?? defaultValue}</textarea>
  )
}

1. Save the file

Copy autosize-textarea.html into templates/components/.

2. Use it

templates/components/autosize-textarea.html
{% from "components/autosize-textarea.html" import autosize_textarea %}

{{ autosize_textarea(name="reply", placeholder="Write a reply…") }}
View source
templates/components/autosize-textarea.html
{# Autosize Textarea macro — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/autosize-textarea.tsx for Python/Flask/FastAPI/Django/Jinja2.

   A <textarea> that grows/shrinks to fit its content between min/max bounds via
   the single CSS rule `field-sizing: content` — no scrollHeight JS hack. See
   repos/mdn/files/en-us/web/css/reference/properties/field-sizing/index.md.
   Tailwind utility: field-sizing-content (repos/tailwindcss/.../utilities.ts).

   Usage:
       {% from "components/autosize-textarea.html" import autosize_textarea %}
       {{ autosize_textarea(name="reply", placeholder="Write a reply…") }}

   Pass autosize=false for a plain bounded textarea (field-sizing: fixed).
   minheight/maxheight are Tailwind height utilities (default min-h-16 / max-h-80). #}

{% macro autosize_textarea(
    id=none,
    name=none,
    value=none,
    placeholder=none,
    required=false,
    disabled=false,
    readonly=false,
    autosize=true,
    minheight="min-h-16",
    maxheight="max-h-80",
    rows=none,
    cols=none,
    minlength=none,
    maxlength=none,
    autocomplete=none,
    autocapitalize=none,
    autocorrect=none,
    autofocus=false,
    spellcheck=none,
    dirname=none,
    wrap=none,
    form=none,
    aria_label=none,
    aria_labelledby=none,
    aria_describedby=none,
    aria_invalid=none,
    aria_required=none,
    extra_class="",
    **attrs
) %}
{%- set base -%}
flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70
{%- endset -%}
{%- set sizing = "field-sizing-content resize-none" if autosize else "field-sizing-fixed resize-y" -%}
<textarea class="{{ base }} {{ sizing }} {{ minheight }} {{ maxheight }} overflow-auto {{ extra_class }}"
          {%- if id %} id="{{ id }}"{% endif %}
          {%- if name %} name="{{ name }}"{% endif %}
          {%- if placeholder %} placeholder="{{ placeholder }}"{% endif %}
          {%- if required %} required{% endif %}
          {%- if disabled %} disabled{% endif %}
          {%- if readonly %} readonly{% endif %}
          {%- if rows is not none %} rows="{{ rows }}"{% endif %}
          {%- if cols is not none %} cols="{{ cols }}"{% endif %}
          {%- if minlength is not none %} minlength="{{ minlength }}"{% endif %}
          {%- if maxlength is not none %} maxlength="{{ maxlength }}"{% endif %}
          {%- if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}
          {%- if autocapitalize %} autocapitalize="{{ autocapitalize }}"{% endif %}
          {%- if autocorrect %} autocorrect="{{ autocorrect }}"{% endif %}
          {%- if autofocus %} autofocus{% endif %}
          {%- if spellcheck is not none %} spellcheck="{{ 'true' if spellcheck else 'false' }}"{% endif %}
          {%- if dirname %} dirname="{{ dirname }}"{% endif %}
          {%- if wrap %} wrap="{{ wrap }}"{% endif %}
          {%- if form %} form="{{ form }}"{% endif %}
          {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
          {%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
          {%- if aria_describedby %} aria-describedby="{{ aria_describedby }}"{% endif %}
          {%- if aria_invalid is not none %} aria-invalid="{{ aria_invalid|string|lower }}"{% endif %}
          {%- if aria_required is not none %} aria-required="{{ aria_required|string|lower }}"{% endif %}
          data-slot="autosize-textarea"
          data-autosize="{{ 'false' if not autosize else 'true' }}"
          {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>{% if value is not none %}{{ value }}{% endif %}</textarea>
{% endmacro %}

1. Save the file

Add autosize-textarea.tmpl alongside your templates.

2. Use it

components/autosize-textarea.tmpl
tpl.ExecuteTemplate(w, "autosize-textarea", map[string]any{
    "Name": "reply",
    "Placeholder": "Write a reply…",
})
View source
components/autosize-textarea.tmpl
{{/*
  Autosize Textarea template — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/autosize-textarea.tsx.

  A <textarea> that grows/shrinks to fit its content between min/max bounds via
  the single CSS rule `field-sizing: content` — no scrollHeight JS hack. See
  repos/mdn/files/en-us/web/css/reference/properties/field-sizing/index.md.
  Tailwind utility: field-sizing-content (repos/tailwindcss/.../utilities.ts).

  Usage:

      type AutosizeTextareaArgs struct {
          ID, Name, Value, Placeholder string
          Required, Disabled, Readonly, Autofocus bool
          Autosize *bool             // tri-state; nil => autosize on
          MinHeight, MaxHeight string // Tailwind height utils
          Rows, Cols int
          MinLength, MaxLength int
          Autocomplete, Autocapitalize, Autocorrect, Wrap, Dirname string
          Spellcheck *bool           // tri-state
          Form, AriaLabel, AriaLabelledby, AriaDescribedby string
          AriaInvalid, AriaRequired string
          Attrs map[string]string
      }

      tpl.ExecuteTemplate(w, "autosize-textarea", AutosizeTextareaArgs{
          Name: "reply", Placeholder: "Write a reply…",
      })

  Pass Autosize=&false for a plain bounded textarea (field-sizing: fixed).
*/}}

{{define "autosize-textarea"}}
{{- $base := "flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&.htmx-request]:opacity-70" -}}
{{- $autosize := true -}}{{- if .Autosize}}{{- $autosize = deref .Autosize -}}{{- end -}}
{{- $sizing := "field-sizing-content resize-none" -}}{{- if not $autosize}}{{- $sizing = "field-sizing-fixed resize-y" -}}{{- end -}}
{{- $minH := or .MinHeight "min-h-16" -}}
{{- $maxH := or .MaxHeight "max-h-80" -}}
<textarea class="{{$base}} {{$sizing}} {{$minH}} {{$maxH}} overflow-auto"
          {{- if .ID}} id="{{.ID}}"{{end}}
          {{- if .Name}} name="{{.Name}}"{{end}}
          {{- if .Placeholder}} placeholder="{{.Placeholder}}"{{end}}
          {{- if .Required}} required{{end}}
          {{- if .Disabled}} disabled{{end}}
          {{- if .Readonly}} readonly{{end}}
          {{- if .Rows}} rows="{{.Rows}}"{{end}}
          {{- if .Cols}} cols="{{.Cols}}"{{end}}
          {{- if .MinLength}} minlength="{{.MinLength}}"{{end}}
          {{- if .MaxLength}} maxlength="{{.MaxLength}}"{{end}}
          {{- if .Autocomplete}} autocomplete="{{.Autocomplete}}"{{end}}
          {{- if .Autocapitalize}} autocapitalize="{{.Autocapitalize}}"{{end}}
          {{- if .Autocorrect}} autocorrect="{{.Autocorrect}}"{{end}}
          {{- if .Autofocus}} autofocus{{end}}
          {{- if .Spellcheck}} spellcheck="{{if deref .Spellcheck}}true{{else}}false{{end}}"{{end}}
          {{- if .Dirname}} dirname="{{.Dirname}}"{{end}}
          {{- if .Wrap}} wrap="{{.Wrap}}"{{end}}
          {{- if .Form}} form="{{.Form}}"{{end}}
          {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
          {{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
          {{- if .AriaDescribedby}} aria-describedby="{{.AriaDescribedby}}"{{end}}
          {{- if .AriaInvalid}} aria-invalid="{{.AriaInvalid}}"{{end}}
          {{- if .AriaRequired}} aria-required="{{.AriaRequired}}"{{end}}
          data-slot="autosize-textarea"
          data-autosize="{{if $autosize}}true{{else}}false{{end}}"
          {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>{{.Value}}</textarea>
{{end}}

1. Save the file

Drop autosize_textarea.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/autosize_textarea.ex
<.autosize_textarea name="reply" placeholder="Write a reply…" />
View source
lib/my_app_web/components/autosize_textarea.ex
defmodule ShadcnHtmx.Components.AutosizeTextarea do
  @moduledoc """
  Autosize Textarea — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/autosize-textarea.tsx. A `<textarea>` that grows and
  shrinks to fit its content between min/max bounds via the single CSS rule
  `field-sizing: content` — no scrollHeight JS hack required. See
  repos/mdn/files/en-us/web/css/reference/properties/field-sizing/index.md and
  the Tailwind utility in repos/tailwindcss/.../utilities.ts (field-sizing-content).

  Where `field-sizing` is unsupported the rule is ignored and the element renders
  as a plain fixed-height textarea — progressive enhancement, not emulation.

  ## Examples

      <.autosize_textarea name="reply" placeholder="Write a reply…" />

      <.autosize_textarea name="comment"
        hx-post="/comments/draft" hx-trigger="input changed delay:500ms" />

      <.autosize_textarea autosize={false} value="A plain bounded textarea." />
  """

  use Phoenix.Component

  @base "flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs " <>
          "transition-[color,box-shadow] outline-none " <>
          "placeholder:text-muted-foreground " <>
          "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
          "disabled:cursor-not-allowed disabled:opacity-50 " <>
          "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
          "md:text-sm dark:bg-input/30 " <>
          "[&.htmx-request]:opacity-70"

  attr :class, :string, default: nil
  attr :value, :string, default: nil
  attr :autosize, :boolean, default: true
  attr :min_height, :string, default: "min-h-16"
  attr :max_height, :string, default: "max-h-80"

  attr :rest, :global,
    include:
      ~w(hx-get hx-post hx-put hx-patch hx-target hx-swap hx-trigger hx-indicator hx-vals hx-include
         id name placeholder required disabled readonly
         rows cols minlength maxlength autocomplete autocapitalize autocorrect autofocus spellcheck dirname wrap form
         aria-label aria-labelledby aria-describedby aria-invalid aria-required)

  def autosize_textarea(assigns) do
    sizing = if assigns.autosize, do: "field-sizing-content resize-none", else: "field-sizing-fixed resize-y"

    assigns =
      assigns
      |> assign(:base_class, @base)
      |> assign(:sizing_class, sizing)

    ~H"""
    <textarea
      class={[@base_class, @sizing_class, @min_height, @max_height, "overflow-auto", @class]}
      data-slot="autosize-textarea"
      data-autosize={to_string(@autosize)}
      {@rest}
    >{@value}</textarea>
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens. field-sizing-content is one Tailwind v4 utility.

2. Use it

snippets/autosize-textarea.html
<textarea name="reply" placeholder="Write a reply…" data-slot="autosize-textarea"
          class="flex w-full rounded-md border border-input bg-transparent px-3 py-2
                 text-base … field-sizing-content resize-none min-h-16 max-h-80 overflow-auto"></textarea>
View source
snippets/autosize-textarea.html
<!--
  shadcn-htmx — raw HTML autosize-textarea snippets.

  Mirrors registry/ui/autosize-textarea.tsx. The `field-sizing: content` rule
  (Tailwind utility field-sizing-content) makes the element grow/shrink to fit
  its content between the min-h-* / max-h-* bounds — no scrollHeight JS hook.
  See repos/mdn/files/en-us/web/css/reference/properties/field-sizing/index.md.

  Where field-sizing is unsupported the rule is ignored and the textarea renders
  as an ordinary fixed-height field (sized by min-height + rows). Progressive
  enhancement, not emulation — relies only on theme tokens.

  BASE:
    flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base
    shadow-xs transition-[color,box-shadow] outline-none
    placeholder:text-muted-foreground
    focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50
    disabled:cursor-not-allowed disabled:opacity-50
    aria-invalid:border-destructive aria-invalid:ring-destructive/20
    dark:aria-invalid:ring-destructive/40
    md:text-sm dark:bg-input/30
    [&.htmx-request]:opacity-70
  AUTOSIZE: field-sizing-content resize-none min-h-16 max-h-80 overflow-auto
  FIXED:    field-sizing-fixed   resize-y   min-h-16 max-h-80 overflow-auto
-->

<!-- Basic — grows as you type, scrolls past max-h-80 -->
<textarea name="reply" placeholder="Write a reply… the field grows as you type"
  data-slot="autosize-textarea" data-autosize="true"
  class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 md:text-sm dark:bg-input/30 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto"></textarea>

<!-- Plain bounded (autosize off) — classic fixed textarea with a drag handle -->
<textarea name="notes" data-slot="autosize-textarea" data-autosize="false"
  class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 md:text-sm dark:bg-input/30 field-sizing-fixed resize-y min-h-16 max-h-80 overflow-auto">A plain bounded textarea.</textarea>

<!-- Disabled -->
<textarea disabled data-slot="autosize-textarea" data-autosize="true"
  class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto">Locked content.</textarea>

<!-- Invalid + describedby -->
<div>
  <textarea name="comment" aria-invalid="true" aria-describedby="comment-error"
            placeholder="Comment…" data-slot="autosize-textarea" data-autosize="true"
    class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto"></textarea>
  <p id="comment-error" class="mt-1 text-sm text-destructive">Comment can't be empty.</p>
</div>

<!-- htmx — autosave draft on input pause -->
<textarea name="draft" placeholder="Start writing… we'll save as you pause"
          data-slot="autosize-textarea" data-autosize="true"
          hx-post="/drafts/123" hx-trigger="input changed delay:600ms" hx-swap="none"
  class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 md:text-sm dark:bg-input/30 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto [&.htmx-request]:opacity-70"></textarea>

Examples

Grow as you type

field-sizing: content shrinkwraps the textarea to its value and grows it line by line — no resize observer, no scrollHeight measuring.

The old way to autosize a textarea was to listen for input events, write the value into a hidden mirror, read its scrollHeight and assign it back as a pixel height on every keystroke. The platform now does this itself: one declaration, field-sizing: content, and the control resizes to its contents. Per MDN, once it is set the rows / cols attributes have no effect — sizing is driven by the text.

<Label htmlFor="reply">Reply</Label>
<AutosizeTextarea id="reply" name="reply"
  placeholder="Write a reply. The field grows as you type…" />
{{ label("Reply", for_="reply") }}
{{ autosize_textarea(id="reply", name="reply",
            placeholder="Write a reply. The field grows as you type…") }}
{{template "label" (dict "For" "reply" "Text" "Reply")}}
{{template "autosize-textarea" (dict
  "ID" "reply" "Name" "reply"
  "Placeholder" "Write a reply. The field grows as you type…"
)}}
<.label for="reply">Reply</.label>
<.autosize_textarea id="reply" name="reply"
  placeholder="Write a reply. The field grows as you type…" />
<div class="grid w-full max-w-md gap-2">
  <label for="ex-reply" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Reply</label>
  <textarea class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&amp;.htmx-request]:opacity-70 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto" data-slot="autosize-textarea" data-autosize="true" id="ex-reply" name="reply" placeholder="Write a reply. The field grows as you type…">
  </textarea>
</div>

Min / max bounds

Pass minHeight / maxHeight (Tailwind height utilities). The field grows freely between them, then scrolls past the max.

MDN is explicit that you should pair field-sizing: content with min-height / max-height, not a fixed height (a fixed height would reimpose a static size and defeat the feature). Once the value reaches the maximum, the browser shows a scrollbar as usual. Defaults here are min-h-16 to max-h-80.

<AutosizeTextarea name="note"
  minHeight="min-h-10" maxHeight="max-h-24" />
{{ autosize_textarea(name="note",
            minheight="min-h-10", maxheight="max-h-24") }}
{{template "autosize-textarea" (dict
  "Name" "note" "MinHeight" "min-h-10" "MaxHeight" "max-h-24"
)}}
<.autosize_textarea name="note"
  min_height="min-h-10" max_height="max-h-24" />
<div class="grid w-full max-w-md gap-2">
  <label for="ex-bounded" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Short note (caps at ~3 lines)</label>
  <textarea class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&amp;.htmx-request]:opacity-70 field-sizing-content resize-none min-h-10 max-h-24 overflow-auto" data-slot="autosize-textarea" data-autosize="true" id="ex-bounded" name="note">
    This field starts small, grows a couple of lines, then scrolls once it hits its max height. Try adding several more lines to see the scrollbar appear.
  </textarea>
</div>

Opt out + graceful fallback

autosize={false} renders a plain bounded textarea (field-sizing: fixed) with a drag handle. The same fallback is what older browsers see automatically.

This is progressive enhancement, not emulation. Browsers that don't understand field-sizing simply ignore the rule and render an ordinary fixed-height textarea sized by min-height — no broken layout, no JS shim. Passing autosize={false} makes that the explicit behaviour everywhere via field-sizing: fixed, restoring the native resize handle.

<AutosizeTextarea name="notes" autosize={false}
  value="A plain bounded textarea." />
{{ autosize_textarea(name="notes", autosize=false,
            value="A plain bounded textarea.") }}
{{template "autosize-textarea" (dict
  "Name" "notes" "Autosize" false "Value" "A plain bounded textarea."
)}}
<.autosize_textarea name="notes" autosize={false}
  value="A plain bounded textarea." />
<div class="grid w-full max-w-md gap-2">
  <label for="ex-fixed-ta" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Notes (fixed, drag to resize)</label>
  <textarea class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&amp;.htmx-request]:opacity-70 field-sizing-fixed resize-y min-h-16 max-h-80 overflow-auto" data-slot="autosize-textarea" data-autosize="false" id="ex-fixed-ta" name="notes">A plain bounded textarea — field-sizing: fixed. Drag the corner handle.</textarea>
</div>

htmx — autosave draft on pause

hx-trigger="input changed delay:600ms" fires after the user stops typing. The server stores the draft and returns 204.

Autosize and autosave compose cleanly: the field grows with the draft while input changed delay:600ms fires only after a pause, so you don't hammer the server on every keystroke. While the request is in flight htmx adds .htmx-request — the base style dims the field to 70% as a subtle "saving" hint.

The field briefly dims while the server records each pause.

<AutosizeTextarea name="draft" placeholder="Start writing…"
  hx-post="/api/drafts" hx-trigger="input changed delay:600ms"
  hx-swap="none" />
{{ autosize_textarea(name="draft", placeholder="Start writing…",
            hx_post="/api/drafts",
            hx_trigger="input changed delay:600ms",
            hx_swap="none") }}
{{template "autosize-textarea" (dict
  "Name" "draft" "Placeholder" "Start writing…"
  "Attrs" (dict
    "hx-post" "/api/drafts"
    "hx-trigger" "input changed delay:600ms"
    "hx-swap" "none"
  )
)}}
<.autosize_textarea name="draft" placeholder="Start writing…"
  hx-post={~p"/api/drafts"} hx-trigger="input changed delay:600ms"
  hx-swap="none" />
<div class="grid w-full max-w-md gap-2">
  <label for="ex-draft" class="flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50" data-slot="label">Draft</label>
  <textarea class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 md:text-sm dark:bg-input/30 [&amp;.htmx-request]:opacity-70 field-sizing-content resize-none min-h-16 max-h-80 overflow-auto" data-slot="autosize-textarea" data-autosize="true" id="ex-draft" name="draft" placeholder="Start writing… we&#39;ll save as you pause" hx-post="/docs/autosize-textarea/save-draft" hx-trigger="input changed delay:600ms" hx-swap="none">
  </textarea>
  <p class="text-xs text-muted-foreground">The field briefly dims while the server records each pause.</p>
</div>

API Reference

<AutosizeTextarea>

PropTypeDefaultDescription
autosizebooleantrue
true sets field-sizing: content so the textarea grows/shrinks to fit its value; false sets field-sizing: fixed — a plain bounded textarea with a drag handle.MDNfield-sizing
minHeightstring"min-h-16"
Lower growth bound as a Tailwind height utility. Per MDN, pair min/max height with field-sizing rather than a fixed height (a fixed height would reimpose a static size).MDNfield-sizing + min/max height
maxHeightstring"max-h-80"
Upper growth bound as a Tailwind height utility. Once the value reaches it the browser shows a scrollbar.
rows / colsnumber
Fallback size only. With field-sizing: content active these have no effect (MDN); they size the control where field-sizing is unsupported.MDN<textarea> rows
minlength / maxlengthnumber
Length bounds. maxlength also stops the field growing once the character limit is reached.
wrap"hard"|"soft"|"off""soft"
Newline handling on submit.
spellcheckboolean
Browser spellchecker.
autocorrect"on"|"off"
WebKit autocorrect (Safari/iOS).
autocapitalize"off"|"on"|"sentences"|"words"|"characters"
Mobile capitalisation hint.
dirnamestring
Submit text direction (ltr/rtl) as a second form field.
idstring
Pairs the input with a <label for>.
namestring
Form field name on submit.
valuestring
Initial value.
placeholderstring
Placeholder text when empty.
requiredbooleanfalse
Native HTML required for form validation.
disabledbooleanfalse
Disable — unfocusable, not submitted.
readonlybooleanfalse
Read-only — focusable + selectable but not editable.
ariaLabelstring
Accessible name when no visible <label>.MDNaria-label
ariaLabelledbystring
Id of a visible element providing the accessible name.MDNaria-labelledby
ariaDescribedbystring
Id of an element describing this control (announced after the name).MDNaria-describedby
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference