Skip to content

Textarea

A versatile multiline text input component with support for labels, validation, character counting, and auto-resize. Perfect for comments, descriptions, and long-form content. Built for Pulse Framework with full reactivity support.

Import

tsx
import { Textarea, Pulse } from '@odyssee/components';

Basic Usage

Code Éditable
Résultat

With Label and Hint

Add labels and helper text for better UX.

Code Éditable
Résultat

Sizes

Five size options control padding and text size: xs, sm, md, lg, and xl.

Code Éditable
Résultat

Rows

Control the initial height with the rows prop.

Code Éditable
Résultat

Required Field

Mark fields as required with visual indicators.

Code Éditable
Résultat

With Error

Display validation errors.

Code Éditable
Résultat

Character Count

Show character count with maximum length.

Code Éditable
Résultat

Auto-Resize

Automatically grow/shrink based on content.

tsx
const autoResizeTextarea = (
  <Textarea
    label="Description"
    autoResize={true}
    minRows={2}
    maxRows={8}
    placeholder="Type to see auto-resize in action..."
  />
);

Disabled State

Disable the textarea.

Code Éditable
Résultat

Readonly State

Make the textarea read-only.

Code Éditable
Résultat

Reactive Value

Control textarea value with Pulse signals.

tsx
const message = Pulse.signal('');

const reactiveTextarea = (
  <div>
    <Textarea
      label="Message"
      value={message}
      onChange={(val) => message(val)}
      placeholder="Type something..."
    />
    
    <div class="mt-4">
      <p class="text-sm text-gray-600">You typed:</p>
      <p class="text-sm font-mono">{message() || '(nothing yet)'}</p>
    </div>
  </div>
);

With Focus Events

Handle focus and blur events.

tsx
const textareaWithEvents = (
  <Textarea
    label="Message"
    placeholder="Focus on me..."
    onFocus={() => console.log('Focused!')}
    onBlur={() => console.log('Blurred!')}
  />
);

Form Validation

Integrate with form validation.

tsx
const FeedbackForm = () => {
  const feedback = Pulse.signal('');
  const error = Pulse.signal('');

  const validate = () => {
    const value = feedback();
    if (value.length === 0) {
      error('Feedback is required');
      return false;
    }
    if (value.length < 10) {
      error('Feedback must be at least 10 characters');
      return false;
    }
    error('');
    return true;
  };

  const handleSubmit = (e: Event) => {
    e.preventDefault();
    if (validate()) {
      console.log('Submitted:', feedback());
    }
  };

  return (
    <form onsubmit={handleSubmit} class="space-y-4">
      <Textarea
        label="Your Feedback"
        value={feedback}
        onChange={(val) => {
          feedback(val);
          validate();
        }}
        error={error()}
        placeholder="Tell us what you think..."
        rows={4}
        required
      />
      <Button type="submit">Submit Feedback</Button>
    </form>
  );
};

Complete Example

Here's a comprehensive comment form with all features:

tsx
import { Textarea, Button, Alert, Pulse } from '@odyssee/components';

const CommentForm = () => {
  const comment = Pulse.signal('');
  const isSubmitting = Pulse.signal(false);
  const error = Pulse.signal('');
  const success = Pulse.signal(false);

  const maxLength = 500;
  const minLength = 10;

  const isValid = Pulse.computed(() => {
    const len = comment().length;
    return len >= minLength && len <= maxLength;
  });

  const handleSubmit = async (e: Event) => {
    e.preventDefault();
    
    if (!isValid()) {
      error(`Comment must be between ${minLength} and ${maxLength} characters`);
      return;
    }

    isSubmitting(true);
    error('');

    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1500));
      
      console.log('Comment submitted:', comment());
      success(true);
      comment('');

      // Hide success message after 3 seconds
      setTimeout(() => success(false), 3000);
      
    } catch (err) {
      error('Failed to submit comment. Please try again.');
    } finally {
      isSubmitting(false);
    }
  };

  return (
    <div class="max-w-2xl">
      {success() && (
        <Alert
          color="success"
          dismissible={true}
          onDismiss={() => success(false)}
          className="mb-4"
        >
          Comment submitted successfully!
        </Alert>
      )}

      <form onsubmit={handleSubmit} class="space-y-4">
        <Textarea
          label="Add a Comment"
          value={comment}
          onChange={(val) => {
            comment(val);
            error('');
          }}
          error={error()}
          placeholder="Share your thoughts..."
          maxLength={maxLength}
          showCount={true}
          autoResize={true}
          minRows={3}
          maxRows={10}
          disabled={isSubmitting()}
          required
        />

        <div class="flex items-center justify-between">
          <p class="text-xs text-gray-500">
            {comment().length >= minLength ? (
              <span class="text-green-600">✓ Minimum length reached</span>
            ) : (
              <span>At least {minLength} characters required</span>
            )}
          </p>

          <div class="flex gap-2">
            <Button
              variant="outline"
              onClick={() => comment('')}
              disabled={isSubmitting() || comment().length === 0}
            >
              Clear
            </Button>
            <Button
              type="submit"
              color="primary"
              loading={isSubmitting()}
              disabled={!isValid()}
            >
              Post Comment
            </Button>
          </div>
        </div>
      </form>
    </div>
  );
};

Props

PropTypeDefaultDescription
valuestring | Signal<string>-Textarea value
placeholderstring""Placeholder text
labelstring-Label text
hintstring-Helper text below input
errorstring-Error message
size"xs" | "sm" | "md" | "lg" | "xl""md"Textarea size
rowsnumber3Initial number of rows
maxLengthnumber-Maximum character count
showCountbooleanfalseShow character counter
autoResizebooleanfalseAuto-resize based on content
minRowsnumber2Minimum rows (auto-resize)
maxRowsnumber10Maximum rows (auto-resize)
disabledbooleanfalseDisable textarea
readonlybooleanfalseMake read-only
requiredbooleanfalseMark as required
onChange(value: string) => void-Change event handler
onFocus(event: FocusEvent) => void-Focus event handler
onBlur(event: FocusEvent) => void-Blur event handler
classNamestring-Additional CSS classes
idstringAuto-generatedHTML id attribute

Accessibility

The Textarea component follows accessibility best practices:

  • ✅ Proper label association with for attribute
  • ✅ ARIA attributes for errors and descriptions
  • ✅ Required field indicators
  • ✅ Keyboard navigation support
  • ✅ Focus management
  • ✅ Screen reader friendly error messages

ARIA Attributes

tsx
const accessibleTextarea = (
  <Textarea
    label="Feedback"
    aria-label="User feedback"
    aria-describedby="feedback-hint"
    aria-invalid={hasError}
  />
);

Best Practices

✅ Do

  • Provide clear labels and hints
  • Show character limits when applicable
  • Use appropriate row heights for expected content
  • Validate input and show helpful error messages
  • Use auto-resize for dynamic content
  • Disable during submission
tsx
// Good: Clear label, validation, and limits
const goodTextarea = (
  <Textarea
    label="Product Review"
    hint="Share your experience with this product"
    maxLength={500}
    showCount={true}
    placeholder="Write your review..."
    required
  />
);

❌ Don't

  • Don't use textarea for single-line input (use Input)
  • Don't forget to handle long text gracefully
  • Don't hide character limits
  • Don't make textareas too small for expected content
  • Don't forget to validate on blur
tsx
// Bad: Too small for expected content
const badTextarea = (
  <Textarea
    label="Essay (500 words)"
    rows={2}
  />
);

// Better: Appropriate size with auto-resize
const betterTextarea = (
  <Textarea
    label="Essay (500 words)"
    autoResize={true}
    minRows={5}
    maxRows={20}
    maxLength={3000}
    showCount={true}
  />
);

Use Cases

Comment Section

tsx
const CommentBox = () => (
  <Textarea
    label="Leave a comment"
    placeholder="What are your thoughts?"
    autoResize={true}
    minRows={2}
    maxRows={8}
  />
);

Feedback Form

tsx
const FeedbackBox = () => (
  <Textarea
    label="How can we improve?"
    hint="Your feedback helps us build better products"
    maxLength={1000}
    showCount={true}
    rows={5}
  />
);

Bio Editor

tsx
const BioEditor = () => (
  <Textarea
    label="About Me"
    hint="Tell others about yourself"
    maxLength={200}
    showCount={true}
    autoResize={true}
    minRows={3}
  />
);

Message Composer

tsx
const MessageComposer = () => (
  <Textarea
    label="Message"
    placeholder="Type your message..."
    autoResize={true}
    minRows={1}
    maxRows={10}
  />
);

Styling & Theming

All textarea styles use Tailwind CSS classes and support dark mode automatically.

Custom Styling

tsx
const customTextarea = (
  <Textarea
    className="font-mono bg-gray-50 dark:bg-gray-800"
    placeholder="Custom styled textarea"
  />
);

TypeScript

Full TypeScript support with complete type definitions:

tsx
import type { TextareaProps } from '@odyssee/components';

const props: TextareaProps = {
  label: 'Message',
  placeholder: 'Type here...',
  maxLength: 500,
  showCount: true,
  autoResize: true,
  onChange: (value: string) => {
    console.log('Value:', value);
  }
};

const textarea = <Textarea {...props} />;

Version: 1.0.0
Last Updated: January 2025

Released under the MIT License.