component

Textarea

A multi-line text field that mirrors the input component — the same feedback color states, size system, background/border variants, and composable parts (<TextareaRoot> + <TextareaField>) with Affix support — but grows vertically. It ships with a rows prop, a native drag-resize handle, and an opt-in autoResize that grows the box to fit its content.

Feedback

Apply semantic feedback colors to indicate validation state or context.

---
import Textarea from "../Textarea.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Textarea feedback="neutral" placeholder="Neutral" />
  <Textarea feedback="primary" placeholder="Primary" />
  <Textarea feedback="success" placeholder="Success" />
  <Textarea feedback="danger" placeholder="Danger" />
  <Textarea feedback="warning" placeholder="Warning" />
</div>

Size

Textareas come in four sizes that share the same height system as inputs and buttons — the size sets the single-row minimum height the box grows from.

---
import Textarea from "../Textarea.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Textarea size="xs" placeholder="Extra Small" />
  <Textarea size="sm" placeholder="Small" />
  <Textarea size="md" placeholder="Medium" />
  <Textarea size="lg" placeholder="Large" />
</div>

Icons & addons (Affix)

To add icons or small controls inside the frame, compose the parts: <TextareaRoot> carries the chrome (size, feedback, background, border) and <TextareaField> is the bare <textarea>; wrap each addon in <Affix>. side="start" (default) pins the addon to the top-left corner, side="end" to the top-right — leaving the bottom-right free for the native resize grabber. The <textarea> fills the whole frame and its text is indented to clear the affixes, so a tall box has no dead gutters. A click anywhere in the frame focuses the field.

Because affixes float over the editable text, keep corner addons compact — an icon, an icon-button. Wide content like a character counter belongs below the field so it never overlaps wrapped text.

---
import TextareaRoot from "../TextareaRoot.astro";
import TextareaField from "../TextareaField.astro";
import Affix from "@pindoba/astro-affix";
import Stamp from "@pindoba/astro-stamp";
import { MessageSquare, Search, Paperclip } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";

const sizes = ["xs", "sm", "md", "lg"] as const;
---

<div class={stack({ gap: "md", direction: "column" })}>
  {
    sizes.map((size) => (
      <TextareaRoot size={size}>
        <Affix>
          <Stamp emphasis="ghost" border="muted">
            <MessageSquare />
          </Stamp>
        </Affix>
        <TextareaField placeholder={`Message (${size})`} />
        <Affix side="end">
          <Stamp emphasis="ghost" border="muted">
            <Paperclip />
          </Stamp>
        </Affix>
      </TextareaRoot>
    ))
  }
  {
    sizes.map((size) => (
      <TextareaRoot size={size}>
        <Affix>
          <Stamp emphasis="ghost" border="muted">
            <Search />
          </Stamp>
        </Affix>
        <TextareaField placeholder={`Search (${size})`} />
      </TextareaRoot>
    ))
  }
</div>

Character count

A counter (or any wide hint) reads best below the field, not overlaying the text. Rather than bake a footer slot into the textarea, pin it with the <Attachment> component — placement="bottom-end" + anchor="outside" floats the counter just below the bottom-right, clear of both the text and the resize grabber.

0 / 280
---
import Textarea from "../Textarea.astro";
import Attachment from "@pindoba/astro-attachment";
import { css } from "@pindoba/styled-system/css";

// The counter floats below the field via the Attachment component. Astro
// renders statically, so the count is illustrative — the Svelte / React / Vue
// tabs update it live.
const MAX = 280;

const field = {
  root: { style: { display: "block", width: "20rem", maxWidth: "100%" } },
};

const counter = css({
  fontSize: "xs",
  fontVariantNumeric: "tabular-nums",
  color: "neutral.text.muted",
  background: "neutral.surface.hill",
  borderRadius: "sm",
  paddingInline: "2xs",
  paddingBlock: "3xs",
});
---

<Attachment placement="bottom-end" anchor="outside" passThrough={field}>
  <Textarea rows={4} fullWidth maxlength={MAX} placeholder="Write a message…" />
  <span slot="content" class={counter}>0 / {MAX}</span>
</Attachment>

Auto-resize

Set autoResize to grow the box to fit its content as the user types. Pair it with resize="none" so the native drag handle doesn’t fight the auto height.

---
import Textarea from "../Textarea.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column" })}>
  <Textarea
    autoResize
    resize="none"
    rows={2}
    placeholder="Type multiple lines…"
    fullWidth
  />
</div>

Custom Styling

The passThrough prop provides escape hatches for each slot: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes.

Custom style via passThrough

Custom props via passThrough

---
import Textarea from "../Textarea.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "xl", direction: "column" })}>
  <div>
    <h3>Custom style via passThrough</h3>
    <Textarea
      placeholder="Custom root style"
      passThrough={{
        root: { style: { borderRadius: "full" } },
        textarea: { style: { fontStyle: "italic" } },
      }}
    />
  </div>

  <div>
    <h3>Custom props via passThrough</h3>
    <Textarea
      placeholder="Message..."
      passThrough={{
        textarea: {
          props: {
            "aria-label": "Message textarea",
            "data-testid": "message-textarea",
          },
        },
      }}
    />
  </div>
</div>
props · 21 shown · 21 total
TextareaRoot
background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Background surface variant for the textarea.

border
"none""bold""default""muted""accent"

Border variant for the textarea wrapper.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color scheme used to indicate validation state or context. `neutral` is the default, `primary` uses the primary color, `success` green, `danger` red, and `warning` orange.

fullWidth
boolean

When true, the textarea stretches to fill the available width of its container.

passThrough
Pick<TextareaPassThrough<RootElementAttributes>, "root">

Per-slot escape hatch for custom styling and props on the wrapper.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override. Without it the corner follows the size tier (`control.<size>`). `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).

size
"sm""md""lg""xs"
default "md"

Size variant. All sizes share the same height system as inputs and buttons (`xs`, `sm`, `md`, `lg`); it sets the minimum (single-row) height.

TextareaField
autoResize
boolean
default false

When true, the textarea auto-grows to fit its content (the framework wires a small resize behavior keyed on `data-auto-resize`). Pair with `resize="none"` so the drag handle doesn't fight the auto height.

passThrough
{ root?: TextareaSlotPassThrough<TextareaElementAttributes> }

Per-slot escape hatch for custom styling and props on the `<textarea>`.

resize
"none""vertical""horizontal""both"
default "vertical"

Which directions the native drag-resize handle allows.

rows
number

Number of visible text rows — sets the initial height of the `<textarea>`.

Textarea
autoResize
boolean
default false

When true, the textarea auto-grows to fit its content.

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"

Background surface variant for the textarea.

border
"none""bold""default""muted""accent"

Border variant for the textarea wrapper.

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Semantic feedback color scheme used to indicate validation state or context. `neutral` is the default, `primary` uses the primary color, `success` green, `danger` red, and `warning` orange.

fullWidth
boolean

When true, the textarea stretches to fill the available width of its container.

passThrough
TextareaPassThrough< RootElementAttributes, TextareaElementAttributes >

Per-slot escape hatch for custom styling and props. Each slot accepts a Panda CSS `style` object and a `props` bag of arbitrary HTML attributes.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"

Corner radius override. Without it the corner follows the size tier (`control.<size>`). `inner` renders a concentric corner computed from the nearest parent panel (parent radius − parent padding, floored at `xs`).

resize
"none""vertical""horizontal""both"
default "vertical"

Which directions the native drag-resize handle allows.

rows
number

Number of visible text rows — sets the initial height of the `<textarea>`.

size
"sm""md""lg""xs"
default "md"

Size variant. All sizes share the same height system as inputs and buttons (`xs`, `sm`, `md`, `lg`); it sets the minimum (single-row) height.

Type

  • Components
  • Blocks