Textarea

Text area let users enter long text into a UI

Usage

Use a textarea when someone needs to enter a long text into a UI.

Instalation

Copy and paste the following code into your project.

import * as React from 'react'
import { forwardRef } from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'

import { cn } from '@/lib/utils'

const labelStyle =
  'pointer-events-none absolute z-10 flex select-none text-label-sm font-normal leading-tight text-onSurfaceVariant duration-200 peer-placeholder-shown/input:text-body-lg peer-placeholder-shown/input:text-onSurfaceVariant/70 peer-focus/input:text-label-sm peer-focus/input:leading-tight peer-focus/input:text-primary peer-disabled/input:!text-onSurfaceVariant/38 group-data-[invalid]/container:text-error'

const outlinedLabel = [
  'h-full w-full left-0 top-[-6px] mr-1 peer-placeholder-shown/input:leading-[4.2]',
  'peer-focus/input:before:mr-1 peer-has-[*]/icon:peer-placeholder-shown/input:before:mr-9 peer-has-[*]/icon:peer-focus/input:before:mr-1',

  /** Before **/
  'before:pointer-events-none before:mr-1 before:mt-[6px] before:box-border before:block before:h-full before:w-3.5 before:rounded-l-sm before:border-outline before:duration-200 before:transition-all before:border-t before:border-l',

  'peer-placeholder-shown/input:before:border-transparent',

  'peer-focus/input:before:border-primary peer-focus/input:before:border-l-2 peer-focus/input:before:border-t-2',

  'group-data-[invalid]/container:before:border-error group-data-[invalid]/container:before:border-l-2 group-data-[invalid]/container:before:border-t-2',

  'peer-disabled/input:before:border-t-onSurface/12 peer-disabled/input:before:border-l-onSurface/12 peer-disabled/input:peer-placeholder-shown/input:before:border-transparent',

  /** After **/
  'after:h-full after:pointer-events-none after:ml-1 after:mt-[6px] after:box-border after:block after:flex-grow after:rounded-r-sm after:border-r after:border-t after:border-outline duration-200 after:transition-all',

  'peer-placeholder-shown/input:after:border-transparent',

  'peer-focus/input:after:border-primary peer-focus/input:after:border-r-2 peer-focus/input:after:border-t-2',

  'group-data-[invalid]/container:after:border-error group-data-[invalid]/container:after:border-r-2 group-data-[invalid]/container:after:border-t-2',

  'peer-disabled/input:after:border-t-onSurface/12 peer-disabled/input:after:border-r-onSurface/12 peer-disabled/input:peer-placeholder-shown/input:after:border-transparent',
]

const FilledTextareaRoot = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
>(({ className, error, ...props }, ref) => (
  <div
    ref={ref}
    data-invalid={error ? '' : undefined}
    className={cn(
      'group/container relative z-0 flex w-full rounded-sm rounded-br-xs bg-surfaceContainer pt-0 before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:rounded-br-xs before:border-b before:border-onSurfaceVariant/12 before:transition-[color,border] focus-within:before:border-b-[2px] focus-within:before:border-primary',
      'has-[:disabled]:bg-surfaceContainer/38 has-[label]:pt-[22px] has-[:disabled]:before:border-onSurface/12',
      Boolean(error) && 'before:border-error focus-within:before:border-error',
      className
    )}
    {...props}
  />
))
FilledTextareaRoot.displayName = 'FilledTextareaRoot'

const OutlinedTextareaRoot = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
>(({ className, error, ...props }, ref) => (
  <div
    ref={ref}
    data-invalid={error ? '' : undefined}
    className={cn(
      'group/container relative z-0 flex w-full bg-transparent before:absolute before:inset-0 before:z-[-1] before:rounded-sm before:rounded-br-xs before:border before:border-outline before:transition-[color,border] focus-within:before:border-2 focus-within:before:border-primary has-[label]:has-[:placeholder-shown]:before:border-outline has-[label]:before:border-t-transparent has-[label]:focus-within:before:border-b-primary has-[label]:focus-within:before:border-t-transparent',
      'has-[label]:pt-2 has-[:disabled]:before:border-onSurface/12 has-[:disabled]:has-[:placeholder-shown]:before:border-onSurface/12 has-[label]:has-[:disabled]:before:border-transparent has-[:disabled]:before:border-t-transparent has-[label]:has-[:disabled]:before:border-b-onSurface/12 has-[label]:has-[:disabled]:before:border-t-transparent',
      Boolean(error) &&
        'before:border-2 before:border-error has-[label]:has-[:placeholder-shown]:before:border-error has-[label]:focus-within:before:border-b-error',
      className
    )}
    {...props}
  />
))
OutlinedTextareaRoot.displayName = 'OutlinedTextareaRoot'

const FilledTextareaLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(
      labelStyle,
      'left-0 top-0 w-full px-4 pb-1 pt-2 peer-placeholder-shown/input:leading-[2.5]',
      className
    )}
    {...props}
  />
))
FilledTextareaLabel.displayName = 'FilledTextareaLabel'

const OutlinedTextfieldLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(labelStyle, outlinedLabel, className)}
    {...props}
  />
))
OutlinedTextfieldLabel.displayName = 'OutlinedTextfieldLabel'

const FilledTextareaInput = forwardRef<
  HTMLTextAreaElement,
  React.InputHTMLAttributes<HTMLTextAreaElement>
>(({ className, placeholder = '', ...props }, ref) => {
  return (
    <>
      <textarea
        ref={ref}
        placeholder={placeholder}
        className={cn(
          'peer/input min-h-[80px] grow resize-y bg-transparent px-4 pb-2 pt-3 text-body-lg text-onSurface caret-primary outline-none transition-opacity placeholder:text-onSurfaceVariant/50 focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 group-has-[label]/container:pt-0 group-has-[:focus]/container:placeholder:opacity-100 group-has-[label]/container:placeholder:opacity-0 group-data-[invalid]/container:caret-error',
          className
        )}
        {...props}
      />
    </>
  )
})

const OutlinedTextareaInput = forwardRef<
  HTMLTextAreaElement,
  React.InputHTMLAttributes<HTMLTextAreaElement>
>(({ className, placeholder = '', ...props }, ref) => {
  return (
    <textarea
      ref={ref}
      placeholder={placeholder}
      className={cn(
        'peer/input min-h-[80px] grow resize-y bg-transparent px-4 pb-2 pt-3 text-body-lg text-onSurface caret-primary outline-none placeholder:text-onSurfaceVariant/50 focus:outline-0 disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-onSurface/38 group-has-[label]/container:pt-1 group-has-[label]/container:placeholder:opacity-0 group-has-[label]/container:focus:placeholder:opacity-100 group-data-[invalid]/container:caret-error',
        className
      )}
      {...props}
    />
  )
})

const SupportingText = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
  return (
    <p
      ref={ref}
      className={cn('mt-1 px-4 text-body-sm text-onSurfaceVariant', className)}
      {...props}
    />
  )
})
SupportingText.displayName = 'SupportingText'

const FilledTextarea = Object.assign(FilledTextareaRoot, {
  Label: FilledTextareaLabel,
  Input: FilledTextareaInput,
  SupportingText,
})

const OutlinedTextarea = Object.assign(OutlinedTextareaRoot, {
  Label: OutlinedTextfieldLabel,
  Input: OutlinedTextareaInput,
  SupportingText,
})

export {
  FilledTextarea,
  OutlinedTextarea,
  FilledTextareaRoot,
  OutlinedTextareaRoot,
  FilledTextareaLabel,
  OutlinedTextfieldLabel,
  FilledTextareaInput,
  OutlinedTextareaInput,
  SupportingText,
}

Update the import paths to match your project setup.

Examples

The component has been built with the idea of maintaining modularity. For this reason, it is VERY IMPORTANT to preserve the correct order in the component structure to ensure they work correctly when used.

The component tree is:

<TextArea>      Text field root container
  <Input />     Text field input
  <Label />     Text field label (optional but recommended)
</TextField>
<SupportingText /> Text field supporting text (optional)

Filled textarea

I’ve chosen a different approach to the border radius than the one suggested in The Material You Guide. I believe this new shape design offers a more modern appearance.

Default

import { FilledTextArea } from '@/components/ui/text-field'

export const FilledTextAreaDefault = () => {
  return (
    <FilledTextarea>
      <FilledTextarea.Input placeholder="Write a description about your project." />
    </FilledTextarea>
  )
}

With label

import { FilledTextArea } from '@/components/ui/text-field'

export const FilledTextareaWithLabel = () => {
  return (
    <FilledTextarea>
      <FilledTextarea.Input placeholder="Write a description about your project." />
      <FilledTextarea.Label>Description</FilledTextarea.Label>
    </FilledTextarea>
  )
}

With supporting text

This is the description of your project.

import { FilledTextArea } from '@/components/ui/text-field'

export const FilledTextareaWithSupportingText = () => {
  return (
    <div className="w-full">
      <FilledTextarea>
        <FilledTextarea.Input placeholder="Write a description about your project." />
        <FilledTextarea.Label>Description</FilledTextarea.Label>
      </FilledTextarea>
      <FilledTextarea.SupportingText>
        This is the description of your project.
      </FilledTextarea.SupportingText>
    </div>
  )
}

Disabled

import { FilledTextArea } from '@/components/ui/text-field'

export const FilledTextareaDisabled = () => {
  return (
    <FilledTextarea>
      <FilledTextarea.Input
        disabled
        placeholder="Write a description about your project."
      />
      <FilledTextarea.Label>Description</FilledTextarea.Label>
    </FilledTextarea>
  )
}

Outlined textarea

Default

import { OutlinedTextArea } from '@/components/ui/text-field'

export const OutlinedTextAreaDefault = () => {
  return (
    <OutlinedTextarea>
      <OutlinedTextarea.Input placeholder="Write a description about your project." />
    </OutlinedTextarea>
  )
}

With label

import { OutlinedTextArea } from '@/components/ui/text-field'

export const OutlinedTextareaWithLabel = () => {
  return (
    <OutlinedTextarea>
      <OutlinedTextarea.Input placeholder="Write a description about your project." />
      <OutlinedTextarea.Label>Description</OutlinedTextarea.Label>
    </OutlinedTextarea>
  )
}

With supporting text

This is the description of your project.

import { OutlinedTextArea } from '@/components/ui/text-field'

export const OutlinedTextareaWithSupportingText = () => {
  return (
    <div className="w-full">
      <OutlinedTextarea>
        <OutlinedTextarea.Input placeholder="Write a description about your project." />
        <OutlinedTextarea.Label>Description</OutlinedTextarea.Label>
      </OutlinedTextarea>
      <OutlinedTextarea.SupportingText>
        This is the description of your project.
      </OutlinedTextarea.SupportingText>
    </div>
  )
}

Disabled

import { OutlinedTextArea } from '@/components/ui/text-field'

export const OutlinedTextareaDisabled = () => {
  return (
    <OutlinedTextarea>
      <OutlinedTextarea.Input
        disabled
        placeholder="Write a description about your project."
      />
      <OutlinedTextarea.Label>Description</OutlinedTextarea.Label>
    </OutlinedTextarea>
  )
}