component — inputs
Textarea
Multi-line text input built on react-aria-components, styled with the quebi design system. Auto-grows with its content and supports placeholder, invalid, and disabled states.
- form
- input
- text
- multiline
- interactive
04 examples
Default
A basic multi-line text input with a placeholder.
States
Default, invalid, and disabled.
Sizes
xs, sm, and the default md — Input's three steps, so a textarea and the input beside it are set in the same type with the same inset. The min-height comes down with the size; the field still grows with its content.
Controlled
Tracks its value and shows a live character count.
0 characters
source
textarea.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import {
composeRenderProps,
TextArea as TextAreaPrimitive,
type TextAreaProps,
} from "react-aria-components"
import { useFieldSizing } from "@/lib/field-size"
import { cn } from "@/lib/utils"
/**
* The field size scale.
*
* The text and the padding are `Input`'s three steps exactly, so a textarea
* and the input beside it are set in the same type with the same inset. The
* `min-h-*` is the part only this control has: a textarea can never be the
* scale's 30 / 38 / 42px, so what the scale buys here is that a small field's
* *first* line is a small field's line, and the floor comes down with it.
*
* Written out rather than imported from `input.tsx`, as in `select.tsx` and
* `number-field.tsx`: three strings are not worth making `Input` a registry
* dependency of this file.
*/
const textareaSizeStyles = {
xs: "text-xs px-(--q-field-px) py-1.5 min-h-16",
sm: "text-sm px-(--q-field-px) py-2 min-h-18",
md: "text-sm px-(--q-field-px) py-2.5 min-h-20",
} as const
type TextareaSize = keyof typeof textareaSizeStyles
/**
* Textarea — quebi design system
*
* Built on react-aria-components. A multi-line text input that auto-grows with
* its content (field-sizing), drawn like `Input`: boxed by default, underlined
* inside `quebi-editorial`; focus doubles the frame without moving anything,
* invalid turns it `danger`.
*/
interface TextareaComponentProps extends TextAreaProps {
/**
* Control height. Matches `Input`'s scale. Left out, it is whatever the
* surrounding surface asked for — a table cell being the one that does. See
* `@/lib/field-size`.
*/
size?: TextareaSize
}
export function Textarea({ className, size: sizeProp, ...props }: TextareaComponentProps) {
const { size } = useFieldSizing({ size: sizeProp })
return (
<span data-slot="control" className="relative block w-full">
<TextAreaPrimitive
{...props}
className={composeRenderProps(className, (resolved) =>
cn(
"field-sizing-content block w-full appearance-none resize-y",
textareaSizeStyles[size],
"text-quebi-fg placeholder:text-quebi-fg-subtle",
"quebi-field",
"transition-[border-color,box-shadow] duration-150",
"focus:outline-none focus:shadow-(--q-field-focus)",
"invalid:[--q-field-edge:var(--q-danger)] focus:invalid:shadow-(--q-field-focus-danger)",
"aria-invalid:[--q-field-edge:var(--q-danger)] focus:aria-invalid:shadow-(--q-field-focus-danger)",
"disabled:opacity-50 disabled:cursor-not-allowed",
resolved,
),
)}
/>
</span>
)
}