Textarea

Material Design 3 textarea component with multiple variants, sizes, and auto-resize functionality

Textarea

Textareas allow users to enter multi-line text input. @duskmoon-dev/core provides a complete set of Material Design 3 textarea variants with features like auto-resize, character counting, and validation states.

Basic Usage

Basic Textarea

Variants

Outlined Textarea (Default)

Outlined textareas have a transparent background with a visible border, making them stand out on any surface.

Outlined Textarea

Filled Textarea

Filled textareas have a solid background with a bottom border, providing a more subtle appearance.

Filled Textarea

Color Variants

Focus colors can be customized using color variants:

Color Variants

Sizes

Three sizes are available:

Textarea Sizes

Resize Options

Control how users can resize the textarea:

Resize Options

Auto-Resize

Automatically adjust height based on content:

Auto-Resize Textarea

Note: Auto-resize requires JavaScript to function. Example implementation:

<textarea class="textarea textarea-auto-resize" placeholder="Auto-growing textarea..."></textarea>

<script>
  const textarea = document.querySelector('.textarea-auto-resize');

  function autoResize() {
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + 'px';
  }

  textarea.addEventListener('input', autoResize);
  // Initialize on load
  autoResize();
</script>

With Label

Standard Label

Textarea with Label

Floating Label

For filled variant with animated label:

Floating Label

Helper Text

Add helpful information below the textarea:

Textarea with Helper Text

Provide a detailed description of your project

Character Counter

Display character count with optional maximum limit:

Character Counter

0 / 200

JavaScript for live counting:

const bioTextarea = document.getElementById('bio');
const charCount = document.querySelector('.textarea-counter');

bioTextarea.addEventListener('input', () => {
  const count = bioTextarea.value.length;
  charCount.textContent = `${count} / 200`;
  charCount.classList.toggle('textarea-counter-exceeded', count > 200);
});

Validation States

Error State

Indicate validation errors:

Error State

This field is required

Success State

Indicate successful validation:

Success State

Thank you for your feedback!

States

Disabled State

Non-interactive textarea:

Disabled State

Read-only State

Display-only content:

Read-only State

Full Width

Ensure textarea spans the full container width:

Full Width Textarea

Complete Examples

Contact Form

Contact Form

We'll get back to you within 24 hours

Feedback Form with Character Limit

Feedback Form

Your opinion matters to us
0 / 500

JavaScript for live counting:

const feedbackTextarea = document.getElementById('feedback');
const feedbackCount = document.querySelector('.textarea-counter');

feedbackTextarea.addEventListener('input', () => {
  const count = feedbackTextarea.value.length;
  feedbackCount.textContent = `${count} / 500`;
  feedbackCount.classList.toggle('textarea-counter-exceeded', count > 500);
});

Auto-Resize Comment Box

Auto-Resize Comment Box

Note: Modern browsers support field-sizing: content for native auto-grow. For older browsers, use JavaScript:

const commentBox = document.getElementById('comment');

function autoResize() {
  commentBox.style.height = 'auto';
  commentBox.style.height = commentBox.scrollHeight + 'px';
}

commentBox.addEventListener('input', autoResize);
autoResize();

Best Practices

Placeholder Text

  • Use placeholders to provide examples, not labels
  • Keep placeholder text concise
  • Don’t rely solely on placeholders for critical information

Placeholder Best Practices

Labels and Helper Text

  • Always provide visible labels for accessibility
  • Use helper text for formatting requirements or examples
  • Place error messages in helper text

Labels and Helper Text

Include key features and goals (minimum 50 characters)

Sizing

  • Set appropriate rows attribute for expected content length
  • Use textarea-sm for compact interfaces
  • Use textarea-lg for primary content input
  • Consider auto-resize for variable-length content

Sizing Best Practices

Accessibility

  • Always provide a <label> element
  • Use aria-describedby for helper text
  • Mark required fields clearly
  • Ensure sufficient color contrast

Accessible Textarea

This field is required. Please provide detailed comments.

Framework Examples

React

import { useState } from 'react';

interface TextareaProps {
  label?: string;
  helperText?: string;
  maxLength?: number;
  variant?: 'outlined' | 'filled';
  size?: 'sm' | 'md' | 'lg';
  error?: boolean;
  success?: boolean;
  autoResize?: boolean;
  placeholder?: string;
  onChange?: (value: string) => void;
}

export function Textarea({
  label,
  helperText,
  maxLength,
  variant = 'outlined',
  size = 'md',
  error = false,
  success = false,
  autoResize = false,
  placeholder,
  onChange,
  ...props
}: TextareaProps) {
  const [value, setValue] = useState('');
  const [height, setHeight] = useState('auto');

  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newValue = e.target.value;
    setValue(newValue);
    onChange?.(newValue);

    if (autoResize) {
      e.target.style.height = 'auto';
      e.target.style.height = `${e.target.scrollHeight}px`;
    }
  };

  const textareaClasses = [
    'textarea',
    variant === 'filled' && 'textarea-filled',
    variant === 'outlined' && 'textarea-outlined',
    size === 'sm' && 'textarea-sm',
    size === 'lg' && 'textarea-lg',
    error && 'textarea-error',
    success && 'textarea-success',
    autoResize && 'textarea-auto-resize'
  ].filter(Boolean).join(' ');

  return (
    <div className="textarea-container">
      {label && (
        <label className="textarea-label">
          {label}
        </label>
      )}
      <textarea
        className={textareaClasses}
        value={value}
        onChange={handleChange}
        maxLength={maxLength}
        placeholder={placeholder}
        {...props}
      />
      <div className="flex justify-between items-center mt-1">
        {helperText && (
          <span className="textarea-helper">{helperText}</span>
        )}
        {maxLength && (
          <div className={`textarea-counter ${value.length > maxLength ? 'textarea-counter-exceeded' : ''}`}>
            {value.length} / {maxLength}
          </div>
        )}
      </div>
    </div>
  );
}

// Usage
<Textarea
  label="Message"
  placeholder="Enter your message..."
  maxLength={200}
  helperText="Provide a detailed message"
  autoResize
/>

Vue

<template>
  <div class="textarea-container">
    <label v-if="label" class="textarea-label">
      {{ label }}
    </label>
    <textarea
      :class="textareaClasses"
      :value="modelValue"
      :placeholder="placeholder"
      :maxlength="maxLength"
      @input="handleInput"
      v-bind="$attrs"
    />
    <div v-if="helperText || maxLength" class="flex justify-between items-center mt-1">
      <span v-if="helperText" class="textarea-helper">{{ helperText }}</span>
      <div
        v-if="maxLength"
        :class="['textarea-counter', { 'textarea-counter-exceeded': charCount > maxLength }]"
      >
        {{ charCount }} / {{ maxLength }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({
  modelValue: String,
  label: String,
  helperText: String,
  maxLength: Number,
  variant: {
    type: String,
    default: 'outlined'
  },
  size: {
    type: String,
    default: 'md'
  },
  error: Boolean,
  success: Boolean,
  autoResize: Boolean,
  placeholder: String
});

const emit = defineEmits(['update:modelValue']);

const charCount = computed(() => props.modelValue?.length || 0);

const textareaClasses = computed(() => [
  'textarea',
  props.variant === 'filled' && 'textarea-filled',
  props.variant === 'outlined' && 'textarea-outlined',
  props.size === 'sm' && 'textarea-sm',
  props.size === 'lg' && 'textarea-lg',
  props.error && 'textarea-error',
  props.success && 'textarea-success',
  props.autoResize && 'textarea-auto-resize'
].filter(Boolean).join(' '));

const handleInput = (e) => {
  emit('update:modelValue', e.target.value);

  if (props.autoResize) {
    e.target.style.height = 'auto';
    e.target.style.height = `${e.target.scrollHeight}px`;
  }
};
</script>

<!-- Usage -->
<Textarea
  v-model="message"
  label="Message"
  placeholder="Enter your message..."
  :max-length="200"
  helper-text="Provide a detailed message"
  auto-resize
/>

API Reference

Class Names

ClassDescription
.textareaBase textarea styles (required)
.textarea-outlinedOutlined variant with transparent background
.textarea-filledFilled variant with solid background
.textarea-primaryPrimary focus color
.textarea-secondarySecondary focus color
.textarea-tertiaryTertiary focus color
.textarea-smSmall size (4rem min-height)
.textarea-lgLarge size (8rem min-height)
.textarea-errorError state styling
.textarea-successSuccess state styling
.textarea-resize-noneDisable resizing
.textarea-resize-verticalAllow vertical resize (default)
.textarea-resize-horizontalAllow horizontal resize
.textarea-resize-bothAllow resizing in both directions
.textarea-auto-resizeAuto-resize with content (requires JS)
.textarea-fullFull width
.textarea-containerWrapper for label and helper text
.textarea-labelLabel styling
.textarea-label-floatingFloating label for filled variant
.textarea-helperHelper text styling
.textarea-counterCharacter counter styling
.textarea-counter-exceededExceeded character limit styling

Container Classes

ClassDescription
.textarea-container-errorError state for container
.textarea-container-successSuccess state for container

Combinations

You can combine classes for different effects:

Class Combinations

  • Input - Single-line text input
  • Select - Dropdown selection
  • Form - Form containers and validation

See Also