Octocat

Textarea

A taller input.

import { Textarea } from '@/components/textarea';

export default function TextareaPreview() {
  return (
    <Textarea
      className="w-80"
      rows={5}
      placeholder="Write your next novel here"
    />
  );
}

Dependencies

Source Code

'use client';

import type { VariantProps } from 'cva';
import { useEffect, useRef, useState } from 'react';

import { inputStyle } from '@/components/input';
import { composeRefs } from '@/lib/compose-refs';
import { cn } from '@/lib/utils/classnames';

interface TextareaProps extends React.ComponentPropsWithRef<'textarea'> {
  invalid?: boolean;
  variant?: VariantProps<typeof inputStyle>['variant'];
}

const Textarea = ({ className, invalid, variant, ...props }: TextareaProps) => {
  return (
    <textarea
      data-invalid={invalid}
      aria-invalid={invalid}
      className={cn(
        inputStyle({ variant }),
        'h-auto resize-none py-2 leading-snug',
        className
      )}
      {...props}
    />
  );
};

// as soon as `field-sizing: content` is supported, we can remove this component and just use the Textarea

/**
 * A textarea that resizes as you type.
 */
const TextareaResize = ({ ref, ...props }: TextareaProps) => {
  const internalRef = useRef<HTMLTextAreaElement>(null);
  const [internalValue, setInternalValue] = useState(props.value);

  const value = props.value ?? internalValue;

  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    setInternalValue(event.target.value);
    props.onChange?.(event);
  };

  useEffect(() => {
    if (internalRef.current) {
      internalRef.current.style.height = 'auto';
      internalRef.current.style.height = `${internalRef.current.scrollHeight}px`;
    }
  }, []);

  return (
    <Textarea
      ref={composeRefs(ref, internalRef)}
      {...props}
      value={value}
      onChange={handleChange}
    />
  );
};

export { Textarea, TextareaResize };

API Reference

Extends the textarea element.

Prop Default Type
variant "default" "default""minimal"
invalid - boolean

Examples

Default

import { Textarea } from '@/components/textarea';

export default function TextareaPreview() {
  return (
    <Textarea
      className="w-80"
      rows={5}
      placeholder="Write your next novel here"
    />
  );
}

Minimal

import { Textarea } from '@/components/textarea';

export default function TextareaMinimalPreview() {
  return (
    <Textarea
      className="w-80"
      variant="minimal"
      rows={5}
      placeholder="Write your next novel here"
    />
  );
}

Disabled

import { Textarea } from '@/components/textarea';

export default function TextareaDisabledPreview() {
  return (
    <Textarea
      className="w-80"
      rows={5}
      disabled
      value="Once upon a time, in a distant galaxy, there lived a lonely star. Each day it would shine brightly, hoping to catch the attention of passing comets. One day, a beautiful comet noticed its radiant glow and decided to orbit nearby. From that day forward, the star was never lonely again."
    />
  );
}

Resizable

This is a cross-browser solution for resizing the textarea. When field-sizing: content is supported, we can remove this component and just use the Textarea.

import { TextareaResize } from '@/components/textarea';

export default function TextareaResizePreview() {
  return (
    <TextareaResize className="w-80" placeholder="Write your next novel here" />
  );
}

Best Practices

  1. Sizing:

    • Consider initial height based on expected content
    • Allow resizing when appropriate
    • Maintain consistent width with other inputs
  2. Accessibility:

    • Provide clear labels
    • Consider character/word count indicators

Previous

Tabs

Next

Tooltip