MC Textarea
A multi-line text input with field integration and block addons.
Overview
The mc-textarea is a multi-line text input for the MicroClub UI library. It follows the same single-component pattern as mc-input — label, description, and error are built in as props. Block addons (top/bottom) support toolbars, character counters, and action bars.
As a part of the MicroClub library, this component is designed for instant deployment into your codebase, giving you full ownership of its look and behavior.
Preview
import { McTextarea } from '../ui/mc-textarea';export default function McTextareaDemo() { return <McTextarea label="Message" placeholder="Type your message..." />;}Add to Your Project
Deploy the mc-textarea directly to your components/ui directory:
Manual Setup
If you prefer manual control:
'use client';import * as React from 'react';import { cn } from '@/lib/utils';export interface McTextareaProps extends React.ComponentProps<'textarea'> { /** Label rendered above the textarea. */ label?: React.ReactNode; /** Help text below the textarea. Replaced by error when present. */ description?: React.ReactNode; /** Truthy = error state. ReactNode = renders the message. `true` = red border only. */ error?: React.ReactNode; /** Block-start addon (toolbar, controls above the textarea). */ addonTop?: React.ReactNode; /** Block-end addon (character count, action buttons below the textarea). */ addonBottom?: React.ReactNode;}function McTextarea({ className, label, description, error, addonTop, addonBottom, id, disabled, required, rows = 3, ...props}: McTextareaProps) { const generatedId = React.useId(); const textareaId = id ?? generatedId; const hasError = !!error; const errorMessage = typeof error === 'boolean' ? null : error; const showError = hasError && !!errorMessage; const showDescription = !hasError && !!description; const messageId = showError ? `${textareaId}-error` : showDescription ? `${textareaId}-description` : undefined; const handleGroupClick = (e: React.MouseEvent<HTMLDivElement>) => { const target = e.target as HTMLElement; if (!target.closest('button, a, input, select, textarea, [tabindex]')) { e.currentTarget.querySelector<HTMLTextAreaElement>('[data-slot="textarea"]')?.focus(); } }; return ( <div data-slot="textarea-root" className={cn('flex flex-col gap-1.5', className)}> {label && ( <label data-slot="textarea-label" htmlFor={textareaId} className="text-sm font-medium text-muted-foreground" > {label} </label> )} <div data-slot="textarea-group" data-error={hasError || undefined} data-disabled={disabled || undefined} role="group" onClick={handleGroupClick} className={cn( // Structure 'group/textarea flex min-w-0 cursor-text flex-col rounded-lg border border-border bg-input shadow-xs transition-all', // Focus ring 'focus-within:ring-4 focus-within:ring-ring', // Error 'data-error:border-destructive', 'data-error:focus-within:border-destructive data-error:focus-within:ring-destructive/20', 'dark:data-error:border-destructive/60 dark:data-error:focus-within:ring-destructive/30', // Disabled 'data-disabled:pointer-events-none data-disabled:opacity-50' )} > {addonTop && ( <div data-slot="textarea-addon" data-align="top" className={cn( 'flex w-full items-center gap-2 border-b border-border px-3.5 py-1.5 text-sm text-muted-foreground select-none', "[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0" )} > {addonTop} </div> )} <textarea data-slot="textarea" id={textareaId} disabled={disabled} required={required} rows={rows} aria-invalid={hasError || undefined} aria-describedby={messageId} className="min-h-0 w-full resize-none bg-transparent px-3.5 py-2.5 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed" {...props} /> {addonBottom && ( <div data-slot="textarea-addon" data-align="bottom" className={cn( 'flex w-full items-center gap-2 border-t border-border px-3.5 py-1.5 text-sm text-muted-foreground select-none', "[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0" )} > {addonBottom} </div> )} </div> {showError && ( <p data-slot="textarea-error" id={`${textareaId}-error`} role="alert" className="text-sm text-destructive" > {errorMessage} </p> )} {showDescription && ( <p data-slot="textarea-description" id={`${textareaId}-description`} className="text-sm text-muted-foreground" > {description} </p> )} </div> );}export { McTextarea };Ensure your @/lib/utils or equivalent classname helper is correctly imported.
Usage
import { McTextarea } from '@/components/ui/mc-textarea';
export default function Example() {
return <McTextarea label="Message" placeholder="Type your message..." />;
}Examples
With Description
<McTextarea label="Bio" description="Tell us about yourself." placeholder="Write something..." />Error State
<McTextarea label="Message" error="Message is required." placeholder="Type your message..." />Character Counter
Use addonBottom to add a live character counter below the textarea.
const [value, setValue] = useState('');
const max = 280;
<McTextarea
label="Post"
addonBottom={
<span className="ml-auto text-xs">
{value.length}/{max}
</span>
}
value={value}
onChange={(e) => setValue(e.target.value.slice(0, max))}
placeholder="What's happening?"
/>;Toolbar
Use addonTop to add a formatting toolbar above the textarea. Buttons can use McInputButton from mc-input.
import { McInputButton } from '@/components/ui/mc-input';
import { Bold, Italic, Link } from 'lucide-react';
<McTextarea
label="Comment"
addonTop={
<div className="flex items-center gap-1">
<McInputButton variant="ghost">
<Bold />
</McInputButton>
<McInputButton variant="ghost">
<Italic />
</McInputButton>
<McInputButton variant="ghost">
<Link />
</McInputButton>
</div>
}
placeholder="Write a comment..."
/>;Action Bar
Combine a character counter and a submit button in addonBottom.
<McTextarea
label="Feedback"
addonBottom={
<div className="flex w-full items-center justify-between">
<span className="text-xs">{value.length} characters</span>
<McInputButton variant="filled">
<Send /> Send
</McInputButton>
</div>
}
placeholder="Tell us what you think..."
/>API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | undefined | Label rendered above the textarea |
description | ReactNode | undefined | Help text below the textarea. Hidden when error is present. |
error | ReactNode | undefined | Error message below the textarea. true = red border only. |
addonTop | ReactNode | undefined | Block-start addon (toolbar, controls above the textarea) |
addonBottom | ReactNode | undefined | Block-end addon (character count, action buttons below) |
rows | number | 3 | Number of visible text rows |
disabled | boolean | false | Disables interactions and applies disabled styles |
required | boolean | false | Marks the textarea as required |
className | string | undefined | Additional CSS classes for the root element |
All standard HTML textarea attributes are also supported.