FoundationsTextarea

The multi-line text field; vertical resize only.

Preview

The thinking

This is the single-line field grown a second axis, and the whole design is about which axis. A text field exists to make the layer between intent and software thin enough that typing just appears, and the moment a field expects a paragraph instead of a word, the one load-bearing decision is which way you are allowed to stretch it. My read is that a drag on the corner is almost always reaching for room to say more, not for a wider column — so I let the field grow down and not across, and a horizontal drag never gets to break the measure the rest of the layout was set to. That conviction is the design; the numbers below just carry it out.

Interface design

resize-y only, with a min-h-[80px] floor — three comfortable lines, so the field looks like it expects a paragraph before anyone types one. Everything else is inherited straight from the single-line field: the --input hairline, the ring on any focus, the placeholder:text-placeholder token. A field that holds more text shouldn't feel like a different kind of thing.

Interaction

The only motion I wrote is transition-colors at duration-150 ease-swift, and it covers exactly what it names — the ring is box-shadow, outside that property list, so focus:ring-1 lands the instant focus does, and it fires on any focus, click or tab alike, because a click into a field is the beginning of typing. The interaction that defines this component I didn't write at all. The corner drag is the browser's own, and my single instruction is the axis: resize-y lets a vertical pull glide and discards the horizontal component entirely — the intent read above, compiled down to one utility. Even the floor does intent work: min-h-[80px] stops an upward drag at three lines, so you can give a thought all the room it wants but never crush the field below the point where you can read what's in it.

Sound design

Silent, and not by exemption. The SoundLayer's capturing click listener speaks only for elements that carry an interactive role — links, buttons, menu items — and text fields were never on its list, so there is no data-silent here to remember. Clicking into a paragraph field is preparation, not commitment, and typing brings its own audio in the keys under your fingers. Even the resize drag stays quiet: it's continuous manipulation with no discrete consequence, and the layer only ever answers a click.

Empathy

Disabled is disabled:opacity-50 plus disabled:cursor-not-allowed — the field keeps taking the pointer so the cursor can say why nothing is happening, and because this renders a real <textarea>, the browser drops it from the tab order on its own. Keyboard users never hit a focus-visible gap: the ring fires on any focus, so tabbing in draws the same ring-ring a click does. Under motion-reduce:transition-none the one color transition snaps, and the resize drag was never an animation to begin with — it tracks the hand directly. What I don't ship is a name: this is a bare field, anonymous to a screen reader until the composer wires a label the way the demo does with htmlFor. And when a paragraph outgrows the box, the browser scrolls it inside — or you drag the corner and give it the room. That second option is the whole component.
textarea.tsx
import * as React from "react";

import { cn } from "@/lib/utils";

export interface TextareaProps
	extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
	ref?: React.Ref<HTMLTextAreaElement>;
}

// resize-y only: users size for more text, never for layout breakage
const Textarea = ({ ref, className, ...props }: TextareaProps) => (
	<textarea
		ref={ref}
		className={cn(
			"flex min-h-[80px] w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm transition-colors duration-150 ease-swift placeholder:text-placeholder focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none",
			className
		)}
		{...props}
	/>
);
Textarea.displayName = "Textarea";

export { Textarea };