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

Loading...
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:

npx mcoli-ui@latest add mc-textarea
pnpm dlx mcoli-ui@latest add mc-textarea
yarn dlx mcoli-ui@latest add mc-textarea
bunx mcoli-ui@latest add mc-textarea

Manual Setup

If you prefer manual control:

Copy the source code
'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 };
Adjust import paths

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

PropTypeDefaultDescription
labelReactNodeundefinedLabel rendered above the textarea
descriptionReactNodeundefinedHelp text below the textarea. Hidden when error is present.
errorReactNodeundefinedError message below the textarea. true = red border only.
addonTopReactNodeundefinedBlock-start addon (toolbar, controls above the textarea)
addonBottomReactNodeundefinedBlock-end addon (character count, action buttons below)
rowsnumber3Number of visible text rows
disabledbooleanfalseDisables interactions and applies disabled styles
requiredbooleanfalseMarks the textarea as required
classNamestringundefinedAdditional CSS classes for the root element

All standard HTML textarea attributes are also supported.

On this page