Component textarea-basic not found in registry.
Component textarea-char-counter not found in registry.
Component textarea-error not found in registry.
Component textarea-disabled not found in registry.
Component textarea-readonly not found in registry.
Component textarea-label-hint not found in registry.
Component textarea-custom-container not found in registry.
Component textarea-long-content not found in registry.
"use client"
// @Diar Muradi Textarea v0.0
import * as React from "react"
import { tv, type VariantProps } from "@diarmuradi/ui/lib/index"
import { composeRenderProps, TextArea, TextField } from "react-aria-components"
const TEXTAREA_ROOT_NAME = "TextareaRoot"
const TEXTAREA_EL_NAME = "Textarea"
const TEXTAREA_RESIZE_HANDLE_NAME = "TextareaResizeHandle"
const TEXTAREA_COUNTER_NAME = "TextareaCounter"
export const textareaVariants = tv({
slots: {
root: [
// base
"group/textarea relative flex w-full flex-col rounded-lg bg-background pb-2.5",
"transition duration-200 ease-out",
// before ring
"before:absolute before:inset-0 before:ring-1 before:ring-inset before:ring-border",
"before:pointer-events-none before:rounded-[inherit]",
"before:transition before:duration-200 before:ease-out",
// hover
"hover:[&:not(:focus-within)]:bg-muted/50",
// disabled
"has-[[disabled]]:pointer-events-none has-[[disabled]]:bg-muted/50 has-[[disabled]]:before:ring-transparent",
],
textarea: [
// base
"block w-full resize-none text-sm text-foreground outline-none",
"pointer-events-auto h-full min-h-[82px] bg-transparent pl-3 pr-2.5 pt-2.5",
// placeholder
"placeholder:select-none placeholder:text-muted-foreground placeholder:transition placeholder:duration-200 placeholder:ease-out",
// hover placeholder
"group-hover/textarea:placeholder:text-foreground/60",
// focus
"focus:outline-none",
// focus placeholder
"focus:placeholder:text-foreground/60",
// disabled
"disabled:text-muted-foreground disabled:placeholder:text-muted-foreground",
],
resizeHandle: [
// base
"pointer-events-none size-3 cursor-s-resize",
],
counter: [
// base
"text-xs text-muted-foreground",
// disabled
"group-has-[[disabled]]/textarea:text-muted-foreground",
],
},
variants: {
hasError: {
true: {
root: [
// base
"before:ring-danger",
// hover
"hover:[&:not(:focus-within)]:before:ring-transparent",
// focus
"has-[textarea:focus-visible]:before:ring-danger",
"has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-danger/10",
"has-[textarea:focus-visible]:ring-offset-2 has-[textarea:focus-visible]:ring-offset-background",
],
counter: "text-destructive",
},
false: {
root: [
// hover
"hover:[&:not(:focus-within)]:before:ring-transparent",
// focus
"has-[textarea:focus-visible]:before:ring-primary",
"has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-primary/10",
"has-[textarea:focus-visible]:ring-offset-2 has-[textarea:focus-visible]:ring-offset-background",
],
},
},
},
defaultVariants: {
hasError: false,
},
})
type TextareaSharedProps = VariantProps<typeof textareaVariants>
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.TextareaHTMLAttributes<HTMLTextAreaElement> & TextareaSharedProps
>(({ className, hasError, disabled, ...rest }, forwardedRef) => {
const { textarea } = textareaVariants({ hasError })
return (
<TextArea
className={composeRenderProps(className, (className) =>
textarea({ class: className })
)}
ref={forwardedRef}
disabled={disabled}
{...rest}
/>
)
})
Textarea.displayName = TEXTAREA_EL_NAME
function ResizeHandle() {
const { resizeHandle } = textareaVariants()
return (
<div className={resizeHandle()}>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.11111 2L2 9.11111M10 6.44444L6.44444 10"
className="stroke-muted-foreground"
/>
</svg>
</div>
)
}
ResizeHandle.displayName = TEXTAREA_RESIZE_HANDLE_NAME
type TextareaRootProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &
TextareaSharedProps & {
children?: React.ReactNode
containerClassName?: string
}
const TextareaRoot = React.forwardRef<HTMLTextAreaElement, TextareaRootProps>(
(
{ containerClassName, children, hasError, className, ...rest },
forwardedRef
) => {
const { root } = textareaVariants({ hasError })
return (
<TextField
className={composeRenderProps(className, (className) =>
root({ class: containerClassName || className })
)}
>
<div className="grid">
<div className="pointer-events-none relative z-10 flex flex-col gap-2 [grid-area:1/1]">
<Textarea ref={forwardedRef} hasError={hasError} {...rest} />
<div className="pointer-events-none flex items-center justify-end gap-1.5 pr-2.5 pl-3">
{children}
<ResizeHandle />
</div>
</div>
<div className="min-h-full resize-y overflow-hidden opacity-0 [grid-area:1/1]" />
</div>
</TextField>
)
}
)
TextareaRoot.displayName = TEXTAREA_ROOT_NAME
function CharCounter({
current,
max,
className,
}: {
current?: number
max?: number
} & React.HTMLAttributes<HTMLSpanElement>) {
const { counter } = textareaVariants()
if (current === undefined || max === undefined) return null
const isError = current > max
return (
<span
className={counter({
class: className,
hasError: isError,
})}
>
{current}/{max}
</span>
)
}
CharCounter.displayName = TEXTAREA_COUNTER_NAME
export { TextareaRoot as Root, CharCounter }
Usage
import * as Textarea from "@/registry/react-aria/ui/textarea"
export default function TextareaExample() {
return (
<Textarea.Root placeholder="Enter your message...">
<Textarea.CharCounter current={value.length} max={100} />
</Textarea.Root>
)
}Examples
Basic Textarea
A simple textarea with placeholder text.
<Textarea.Root placeholder="Enter your message..." />With Character Counter
Display a character count with optional maximum limit.
<Textarea.Root
placeholder="Type something... (max 100 characters)"
maxLength={100}
>
<Textarea.CharCounter current={value.length} max={100} />
</Textarea.Root>With Error State
Show error styling when validation fails.
<Textarea.Root hasError placeholder="This textarea has an error state..." />Disabled State
A disabled textarea that cannot be interacted with.
<Textarea.Root isDisabled placeholder="This textarea is disabled..." />Readonly State
A readonly textarea that displays content but cannot be edited.
<Textarea.Root
isReadOnly
value="This is readonly content that cannot be edited"
/>With Label and Hint
Include a label and hint text for better accessibility.
import * as Hint from "@/registry/react-aria/ui/hint"
import * as Label from "@/registry/react-aria/ui/label"
;<div className="space-y-2">
<Label.Root htmlFor="description">Description</Label.Root>
<Textarea.Root id="description" placeholder="Enter a description..." />
<Hint.Root>Provide a detailed description of your project.</Hint.Root>
</div>Size Variants
Choose from different size options.
<Textarea.Root size="sm" placeholder="Small textarea..." />
<Textarea.Root size="md" placeholder="Medium textarea..." />
<Textarea.Root size="lg" placeholder="Large textarea..." />Resize Controls
Control how the textarea can be resized.
<Textarea.Root resize="none" placeholder="Cannot be resized..." />
<Textarea.Root resize="vertical" placeholder="Vertical resize only..." />
<Textarea.Root resize="horizontal" placeholder="Horizontal resize only..." />
<Textarea.Root resize="both" placeholder="Both directions..." />Resize Handle Control
Show or hide the resize handle.
<Textarea.Root showResizeHandle placeholder="Shows resize handle..." />
<Textarea.Root showResizeHandle={false} placeholder="No resize handle..." />Auto Resize
Automatically adjust height based on content.
<Textarea.Root autoResize placeholder="Type to see auto resize..." />
<Textarea.Root autoResize={{ minRows: 2, maxRows: 6 }} placeholder="Custom min/max rows..." />