- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Markdown is supported.
Installation
$ pnpm dlx shadcn@latest add @cobalt/textarea
Sizes
default and sm match the same sizes of Input, so the two stack on one edge. rows sets the resting height.
Default is 14px type with 12px horizontal inset.
Small tightens the horizontal inset to match a small input.
Auto resize
autoResize grows the field one line at a time up to maxRows, then scrolls. The height is measured from zero on every change, so it shrinks again when text is deleted.
The field grows to eight lines, then scrolls.
Character count
Pair maxLength with a counter next to the description. The browser enforces the limit.
Appears on your public profile.
49/160States
Hover and focus fade the ring in. disabled and aria-invalid override both. Use resize="none" for a fixed height.
This is a hint about this field.
This is a hint about this field.
This is a hint about this field.
Options
variant="ghost"removes the fill, padding, radius and rings, for inline editing. It matchesInput.
Source
"use client";
import { cn } from "cn";
import * as React from "react";
type TextareaProps = React.ComponentProps<"textarea"> & {
autoResize?: boolean;
maxRows?: number;
resize?: "none" | "vertical";
size?: "default" | "sm";
/** `ghost` removes the fill, padding and rings, for inline editing. Matches Input. */
variant?: "default" | "ghost";
};
function Textarea({
className,
rows = 3,
autoResize = false,
maxRows,
resize = "vertical",
size = "default",
variant = "default",
onInput,
ref,
...props
}: TextareaProps) {
const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
const fit = () => {
const field = innerRef.current;
if (!field) {
return;
}
if (!autoResize) {
field.style.height = "";
field.style.maxHeight = "";
field.style.overflowY = "";
return;
}
const line =
Number.parseFloat(window.getComputedStyle(field).lineHeight) || 20;
const padding = 16;
const ceiling = maxRows ? maxRows * line + padding : Infinity;
field.style.height = "0px";
const next = Math.max(field.scrollHeight, rows * line + padding);
field.style.height = `${Math.min(next, ceiling)}px`;
field.style.maxHeight = maxRows ? `${ceiling}px` : "";
field.style.overflowY = next > ceiling ? "auto" : "hidden";
};
React.useLayoutEffect(fit);
return (
<textarea
ref={(node) => {
innerRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
}}
data-slot="textarea"
data-variant={variant}
rows={rows}
className={cn(
// cobalt-allow-focus-within -- text input
"block w-full min-w-0 rounded-[10px] border-0 bg-[var(--surface-strong,var(--muted))] py-2 font-sans text-sm leading-5 text-foreground ring-2 ring-transparent ring-inset outline-none transition-[background-color,box-shadow,color] duration-150 ease-[ease] motion-reduce:transition-none placeholder:text-[color:var(--foreground-subtle,var(--muted-foreground))] hover:not-focus:not-disabled:not-aria-invalid:ring-[var(--border-hover,var(--border))] focus:not-disabled:not-aria-invalid:ring-[var(--foreground-subtle,var(--ring))] focus:placeholder:text-foreground disabled:cursor-not-allowed disabled:bg-[var(--input-disabled-surface,var(--muted))] disabled:text-[color:var(--input-disabled-foreground,var(--muted-foreground))] disabled:placeholder:text-[color:var(--input-disabled-foreground,var(--muted-foreground))] aria-invalid:bg-[var(--input-invalid-surface,color-mix(in_srgb,var(--destructive)_15%,transparent))] aria-invalid:placeholder:text-[color:var(--input-invalid-placeholder,var(--destructive))]",
size === "sm" ? "px-2.5" : "px-3",
variant === "ghost" &&
// cobalt-allow-focus-within -- text input
"rounded-none bg-transparent p-0 hover:not-focus:not-disabled:not-aria-invalid:ring-transparent focus:not-disabled:not-aria-invalid:ring-transparent disabled:bg-transparent focus:placeholder:text-[color:var(--foreground-subtle,var(--muted-foreground))]",
autoResize || resize === "none" ? "resize-none" : "resize-y",
className
)}
onInput={(event) => {
fit();
onInput?.(event);
}}
{...props}
/>
);
}
export { Textarea };