A multi-line text field that can grow with its content between minRows and maxRows, then scroll. Plain resizable mode when autoResize is off.
We read every message. Please don't include passwords.
import { Label } from "@/components/ballmac/label"
import { Textarea } from "@/components/ballmac/textarea"
export default function TextareaDemo() {
return (
<div className="grid w-full max-w-sm gap-2">
<Label htmlFor="feedback">Feedback</Label>
<Textarea id="feedback" placeholder="What could we do better?" rows={4} aria-describedby="feedback-hint" />
<p id="feedback-hint" className="text-sm text-muted-foreground">
We read every message. Please don't include passwords.
</p>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/textareaCopy the source into your project.
components/ballmac/textarea.tsx// Ballmac UI: Textarea. https://ui.ballmac.com/components/textarea // Based on shadcn/ui's Textarea (MIT, Copyright (c) 2023 shadcn), restyled and extended with auto-resize. "use client" import * as React from "react" import { cn } from "@/lib/utils" type TextareaProps = React.ComponentProps<"textarea"> & { /** Grow and shrink with the content instead of scrolling. */ autoResize?: boolean /** Smallest height in rows when autoResize is on. Defaults to `rows` or 3. */ minRows?: number /** Largest height in rows when autoResize is on; past it the textarea scrolls. */ maxRows?: number } const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect function Textarea({ className, autoResize = false, minRows, maxRows, rows, ref, onChange, ...props }: TextareaProps) { const innerRef = React.useRef<HTMLTextAreaElement | null>(null) const min = minRows ?? rows ?? 3 const resize = React.useCallback(() => { const el = innerRef.current if (!el || !autoResize) return const cs = window.getComputedStyle(el) const px = (value: string) => parseFloat(value) || 0 const lineHeight = px(cs.lineHeight) || px(cs.fontSize) * 1.5 || 24 const border = px(cs.borderTopWidth) + px(cs.borderBottomWidth) const chrome = px(cs.paddingTop) + px(cs.paddingBottom) + border const minHeight = min * lineHeight + chrome const maxHeight = maxRows ? maxRows * lineHeight + chrome : Infinity el.style.height = "auto" const content = el.scrollHeight + border el.style.height = `${Math.min(Math.max(content, minHeight), maxHeight)}px` el.style.overflowY = content > maxHeight + 1 ? "auto" : "hidden" }, [autoResize, min, maxRows]) // Re-measure on mount, on controlled value changes and when the width changes (line wrapping). useIsomorphicLayoutEffect(() => { resize() }) React.useEffect(() => { const el = innerRef.current if (!el || !autoResize || typeof ResizeObserver === "undefined") return let width = el.offsetWidth const observer = new ResizeObserver(() => { if (el.offsetWidth !== width) { width = el.offsetWidth resize() } }) observer.observe(el) return () => observer.disconnect() }, [autoResize, resize]) const setRef = React.useCallback( (node: HTMLTextAreaElement | null) => { innerRef.current = node if (typeof ref === "function") ref(node) else if (ref) ref.current = node }, [ref] ) return ( <textarea ref={setRef} data-slot="textarea" data-autoresize={autoResize || undefined} rows={autoResize ? min : rows} onChange={(event) => { onChange?.(event) resize() }} className={cn( "flex min-h-16 w-full min-w-0 rounded-md border border-input bg-background px-3 py-2 text-sm leading-6 text-foreground shadow-xs outline-none transition-[color,border-color,box-shadow] duration-150 selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30", autoResize && "min-h-0 resize-none", className )} {...props} /> ) } export { Textarea, type TextareaProps }Update the import paths to match your project setup.
Usage
import { Textarea } from "@/components/ballmac/textarea"The full example is in the Code tab above.
Examples
Auto-resize
"use client"
import * as React from "react"
import { ArrowUp } from "lucide-react"
import { Button } from "@/components/ballmac/button"
import { Textarea } from "@/components/ballmac/textarea"
export default function TextareaAutosize() {
const [value, setValue] = React.useState("")
return (
<form
className="flex w-full max-w-md items-end gap-2 rounded-xl border bg-card p-2"
onSubmit={(event) => {
event.preventDefault()
setValue("")
}}
>
<Textarea
autoResize
minRows={1}
maxRows={6}
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="Write a message. The box grows up to six lines."
aria-label="Message"
className="border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
/>
<Button type="submit" size="icon-sm" aria-label="Send message" disabled={!value.trim()}>
<ArrowUp />
</Button>
</form>
)
}API reference
| Prop | Type | Default |
|---|---|---|
autoResizeGrow and shrink with the content instead of scrolling. | boolean | false |
minRowsSmallest height in rows when autoResize is on. Defaults to `rows` or 3. | number | — |
maxRowsLargest height in rows when autoResize is on; past it the textarea scrolls. | number | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves focus to the textarea |
| Enter | Inserts a new line |
Use with AI
Multi-line text input. Set autoResize with minRows and maxRows for chat composers and comment boxes; leave it off for a user-resizable box with a fixed rows count. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Textarea (@ballmac/textarea) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Comments, feedback and descriptions
- Chat or prompt composers that grow as the user types (autoResize)
- Notes fields in settings forms
Not for
- Single-line values (use input)
- Rich text or markdown editing with formatting controls
Registry JSON: https://ui.ballmac.com/r/textarea.json
Credits
Based on shadcn/ui Textarea, adapted by Ballmac. Free to use in personal and commercial projects.
- npm
- None
- Registry
- shadcn/utils
Pairs well with
Label
An accessible form label built on Radix Label that dims itself when the paired peer control is disabled and never selects text on double-click.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Dialog
A Radix modal dialog with a blurred overlay, centered card surface that fits 360 px screens, header and footer parts, optional close button and animations.
Accordion
Vertically stacked disclosure sections on Radix Accordion, single or multiple open, with hairline dividers, a rotating plus and height animation.