Textarea
A field for several lines of text that grows as you write, between a smallest and a largest height.
- Add a line to the release notes: the field grows with it, up to 8 rows, then scrolls inside.
- There is no resize handle; the text sets the height.
- Shorten the commit's first line to 72 characters: the error turns back into the hint.
Field marks
- Import
import { Textarea } from "cennge-ui";- Size
- 3.3 KB gzip for this import, its dependencies aside
- Pattern
- Native
<textarea>with a<label> - State
- Controlled with
valueandonValueChange, or uncontrolled withdefaultValue - SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Height
- From
minRows(3) tomaxRows(8) lines, then it scrolls - Resize handle
- None: the height follows the text
- Tests
- Unit tests
- Source
- src/components/textarea
When to use#
Use a Textarea for text that runs to more than a line: release notes, a comment, a description. It starts at minRows lines and grows with the text up to maxRows, so a short note takes little room and a long one does not push the page down without end.
Similar#
- Input is one line, with a clear button and room for icons; a Textarea has neither.
- TagsInput turns typed words into separate tags instead of keeping one block of text.
Rows to grow between#
minRows sets the height of the empty field and maxRows the most it grows; past that the text scrolls inside the field. Both count lines of text.
import { Textarea } from "cennge-ui";
const why = [
"Dark-first, tactile look.",
"Recolour without a rebuild.",
"Updates instead of copy-paste.",
"Accessible by default.",
"Light on dependencies.",
"Server Components ready.",
].join("\n");
export default function ReadmeSection() {
return (
<Textarea
label="Why cennge-ui"
minRows={2}
maxRows={4}
defaultValue={why}
description="Two rows when empty, four at most; scroll for the rest"
className="w-full max-w-md"
/>
);
}Count characters#
description takes any node, so a live count fits there. While isInvalid, errorMessage takes its place.
import { useState } from "react";
import { Textarea } from "cennge-ui";
const limit = 160;
export default function RepositoryDescription() {
const [description, setDescription] = useState("Crypto project screener: FastAPI + CoinGecko backend, Next.js + shadcn/ui frontend");
const over = description.length - limit;
return (
<Textarea
label="Description"
minRows={2}
value={description}
onValueChange={setDescription}
description={`${description.length} / ${limit}`}
isInvalid={over > 0}
errorMessage={`${over} ${over === 1 ? "character" : "characters"} over the limit`}
className="w-full max-w-md"
/>
);
}Send with a shortcut#
Every other prop goes to the <textarea>, onKeyDown included. Here Ctrl+Enter or ⌘+Enter posts the comment, while Enter alone still starts a new line.
import { useState } from "react";
import { Button, Kbd, Textarea, toast } from "cennge-ui";
export default function CommentBox() {
const [comment, setComment] = useState("");
function send() {
if (!comment.trim()) return;
toast.success("Comment posted", { description: "On ai-niche-scout" });
setComment("");
}
return (
<div className="flex w-full max-w-md flex-col gap-3">
<Textarea
label="Comment"
placeholder="Leave a comment"
minRows={2}
maxRows={6}
value={comment}
onValueChange={setComment}
onKeyDown={(event) => {
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
send();
}
}}
description={
<>
<Kbd size="sm" keys={["ctrl", "enter"]} /> or <Kbd size="sm" keys={["command", "enter"]} /> sends it
</>
}
/>
<Button color="primary" className="self-end" isDisabled={!comment.trim()} onClick={send}>
Comment
</Button>
</div>
);
}Accessibility#
The label is a <label for> the textarea, and the description or the error is its aria-describedby. isInvalid sets aria-invalid and isRequired the native required attribute, so inside a Form a named, required field that is empty shows the browser's message under it.
Styling#
It shares the Input's box: the same variant, color, size and radius. className goes to the outer wrapper; classNames reaches base, label, inputWrapper, input, description and errorMessage.
Props#
Also takes every attribute of <textarea>, except color, value, defaultValue, which mean something else here.