A paper-like multiline field with an optional character counter.
0 / 240 characters
pnpm dlx shadcn@latest add @latheui/textareaFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Add the Lathe items this component imports.
hooks/use-controllable-state.tsCopy the source into your project.
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
export type TextareaProps = Omit<
React.ComponentProps<"textarea">,
"value" | "defaultValue"
> & {
value?: string
defaultValue?: string
label: string
counter?: boolean
/** Let people drag the corner to make the field taller. */
expandable?: boolean
/** Tallest the field can be dragged, in pixels or any CSS length. */
maxHeight?: number | string
}
export function Textarea({
label,
counter = false,
expandable = true,
maxHeight,
value: valueProp,
defaultValue = "",
onChange,
id: idProp,
className,
style,
maxLength,
...props
}: TextareaProps) {
const generated = React.useId()
const id = idProp ?? generated
const [value, setValue] = useControllableState({
value: valueProp,
defaultValue,
})
return (
<div className="flex w-full flex-col gap-1.5">
<label htmlFor={id} className="pb-1 text-sm font-medium">
{label}
</label>
<textarea
{...props}
id={id}
maxLength={maxLength}
style={{ maxHeight, ...style }}
value={value}
onChange={(e) => {
setValue(e.target.value)
onChange?.(e)
}}
aria-describedby={
[props["aria-describedby"], counter ? id + "-count" : ""]
.filter(Boolean)
.join(" ") || undefined
}
className={cn(
"block min-h-28 w-full rounded-md border border-input bg-background px-3 py-2 text-base outline-none transition-[border-color,box-shadow] duration-150 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 disabled:opacity-50 motion-reduce:transition-none md:text-sm",
expandable ? "resize-y" : "resize-none",
className
)}
/>
{counter && (
<p
id={id + "-count"}
className="text-right text-xs text-muted-foreground tabular-nums"
>
{value.length}
{maxLength !== undefined ? " / " + maxLength : ""} characters
</p>
)}
</div>
)
}Depends on
| Prop | Type | Default | Description |
|---|---|---|---|
| label* (required) | string | — | Visible label. |
| counter | boolean | false | Display character count. |
| expandable | boolean | true | Let people drag the corner to make the field taller. |
| maxHeight | number | string | — | Tallest the field can be dragged, in pixels or any CSS length. |
| value / defaultValue | string | — | Controlled text or initial text. |
| maxLength | number | — | Native character limit. |
Combine the control with your own layout. Semantic colors follow the active theme.
import { Textarea } from "@/components/ui/textarea"
export default function Demo(){return <div className="w-full max-w-sm"><Textarea label="Project notes" placeholder="What are you working on?" counter maxLength={240}/></div>}