component
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.
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>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>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>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.
---
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>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>The passThrough prop provides escape hatches for each slot: style accepts a Panda CSS SystemStyleObject and props forwards arbitrary HTML attributes.
---
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>Background surface variant for the textarea.
Border variant for the textarea wrapper.
"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.
When true, the textarea stretches to fill the available width of its container.
Per-slot escape hatch for custom styling and props on the wrapper.
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`).
"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.
falseWhen 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.
Per-slot escape hatch for custom styling and props on the `<textarea>`.
"vertical"Which directions the native drag-resize handle allows.
Number of visible text rows — sets the initial height of the `<textarea>`.
falseWhen true, the textarea auto-grows to fit its content.
Background surface variant for the textarea.
Border variant for the textarea wrapper.
"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.
When true, the textarea stretches to fill the available width of its container.
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.
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`).
"vertical"Which directions the native drag-resize handle allows.
Number of visible text rows — sets the initial height of the `<textarea>`.
"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.