Skip to main content

Textarea

Edit multi-line text with optional auto-sizing and Field integration.

Use Textarea for multi-line native text entry. Use InputGroup to put controls around it without changing which element receives typing and focus.

Basic usage#

import { Field, Textarea } from 'moraine'

export function Example() {
  return (
    <Field label="Message">
      <Textarea placeholder="Write a message" />
    </Field>
  )
}

Playground#

Props
Slots

Anatomy#

Textarea [component; slot=root; <textarea>]

Usage#

Native text behavior#

Textarea forwards native textarea attributes and works with controlled or uncontrolled values. Use readOnly to preserve selection and focus without edits, or disabled to prevent interaction.

import { Textarea } from 'moraine'
import { createSignal } from 'solid-js'

export function NativeBehavior() {
  const [bio, setBio] = createSignal('Frontend developer passionate about accessible web UI.')

  return (
    <div class="max-w-md w-full space-y-3">
      <Textarea value={bio()} onValueChange={setBio} rows={3} />
      <p class="text-xs text-muted-foreground">
        Length: <span class="text-foreground font-mono">{bio().length}</span> characters
      </p>
    </div>
  )
}

Size and composition#

Enable autoresize and constrain it with rows and maxRows. Use InputGroup orientation="vertical" with Leading and Trailing for header and footer regions. Counters, submit buttons, and keyboard shortcuts remain application-owned behavior.

import { Button, Textarea, InputGroup } from 'moraine'

export function Composition() {
  return (
    <div class="max-w-md w-full">
      <InputGroup orientation="vertical">
        <Textarea placeholder="Write a comment..." autoResize rows={2} maxRows={6} />
        <InputGroup.Trailing>
          <div class="flex w-full items-center justify-between">
            <span class="text-xs text-muted-foreground">Markdown supported</span>
            <Button size="xs">Comment</Button>
          </div>
        </InputGroup.Trailing>
      </InputGroup>
    </div>
  )
}

Examples#

Autoresize#

import { Textarea } from 'moraine'
import { createSignal } from 'solid-js'

export function Autoresize() {
  const [post, setPost] = createSignal(
    'Just deployed the new Moraine component library docs! Smooth animations, fully typed SolidJS components, and accessible primitives out of the box 🚀',
  )
  const maxLength = 280

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-xl space-y-3">
      <label class="text-xs text-muted-foreground font-medium block">
        Draft Release Announcement
      </label>
      <Textarea
        autoResize
        rows={3}
        maxRows={8}
        value={post()}
        onValueChange={(next) => setPost(next ?? '')}
        placeholder="What's happening in your project?"
      />
      <div class="text-xs flex items-center justify-between">
        <span class="text-muted-foreground">Auto-expands as you type up to 8 rows.</span>
        <span
          class={`font-medium font-mono ${
            post().length > maxLength ? 'text-destructive' : 'text-muted-foreground'
          }`}
        >
          {post().length} / {maxLength}
        </span>
      </div>
    </div>
  )
}
import { Button, Icon, InputGroup, Textarea } from 'moraine'
import { createSignal } from 'solid-js'

export function HeaderFooter() {
  const [value, setValue] = createSignal('Hello Moraine!')

  return (
    <div class="gap-6 grid w-full lg:grid-cols-3">
      <InputGroup orientation="vertical">
        <InputGroup.Leading class="border-b border-border">
          <span class="font-semibold">Info text</span>
          <Icon name="i-lucide:info" class="text-base ms-auto" />
        </InputGroup.Leading>
        <Textarea placeholder="Ask, search or chat..." class="min-h-24" />
      </InputGroup>
      <InputGroup orientation="vertical">
        <Textarea
          value={value()}
          onValueChange={(next) => setValue(next ?? '')}
          placeholder="Write your message..."
          autoResize
          class="min-h-24"
        />
        <InputGroup.Trailing class="border-t border-border">
          <span>{value().length}/280 characters</span>
          <Button type="button" size="sm" class="ms-auto">
            Send
          </Button>
        </InputGroup.Trailing>
      </InputGroup>
      <InputGroup orientation="vertical">
        <InputGroup.Leading class="border-b border-border">
          <Icon name="i-lucide:code" class="text-base" />
          <span>script.js</span>
        </InputGroup.Leading>
        <Textarea placeholder="console.log('Hello, world!');" class="min-h-28" />
        <InputGroup.Trailing class="border-t border-border">
          <span>Line 1, Column 1</span>
          <span class="ms-auto">JavaScript</span>
        </InputGroup.Trailing>
      </InputGroup>
    </div>
  )
}

Character count#

import { Textarea, InputGroup } from 'moraine'
import { createSignal } from 'solid-js'

const MAX_LENGTH = 160

export function CharacterCount() {
  const [value, setValue] = createSignal('')

  return (
    <InputGroup orientation="vertical">
      <Textarea
        value={value()}
        maxLength={MAX_LENGTH}
        onValueChange={setValue}
        placeholder="Add a short description..."
      />
      <InputGroup.Trailing>
        <span class="text-xs text-muted-foreground">
          {value().length}/{MAX_LENGTH}
        </span>
      </InputGroup.Trailing>
    </InputGroup>
  )
}

Form integration#

import { Button, createForm, Textarea } from 'moraine'
import { createSignal } from 'solid-js'
import * as v from 'valibot'

export function FormIntegration() {
  const [submittedFeedback, setSubmittedFeedback] = createSignal('')
  const form = createForm({
    schema: v.object({
      feedback: v.pipe(
        v.string(),
        v.minLength(10, 'Feedback must be at least 10 characters long.'),
      ),
    }),
    initialInput: { feedback: '' },
    validate: 'input',
  })

  return (
    <form.Form onSubmit={(output) => setSubmittedFeedback(output.feedback)}>
      <div class="max-w-xl space-y-4">
        <form.Field
          name="feedback"
          label="Product Feedback"
          description="Tell us what features or fixes you'd like to see next."
          required
        >
          <Textarea placeholder="Write your detailed feedback here..." rows={4} autoResize />
        </form.Field>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Send Feedback
          </Button>
          <p class="text-xs text-muted-foreground">
            Characters submitted: {submittedFeedback().length}
          </p>
        </div>
      </div>
    </form.Form>
  )
}

Attributes#

Attributes
data-autoresizeSlot: textareaDescription: Present when the text control grows with its content.
data-disabledSlot: textareaDescription: Present when the component, slot, or item is disabled.
data-invalidSlot: textareaDescription: Present when the field or form has a validation error.
data-readonlySlot: textareaDescription: Present when the field is in read-only mode.
data-requiredSlot: textareaDescription: Present when the field input is required.

Props#

Props for the Textarea component. Renders a <textarea> element by default.

Prop