Skip to content
Ndop UI

Textarea

Displays a multi-line text field, or a component that looks like one.

  • v1.0.0
  • SSR
  • Zoneless
  • 1.8 kB gzip

Your message goes to the support team.

import { Component } from '@angular/core';
import { Label } from '@/ui/label/label';
import { Textarea } from '@/ui/textarea/textarea';

/** A textarea with a label and a description. */
@Component({
  selector: 'example-textarea-demo',
  imports: [Label, Textarea],
  template: `
    <div class="grid w-full max-w-sm gap-3">
      <label ndLabel for="textarea-demo-message">Your message</label>
      <textarea
        ndTextarea
        id="textarea-demo-message"
        placeholder="Type your message here."
        aria-describedby="textarea-demo-hint"
      ></textarea>
      <p id="textarea-demo-hint" class="text-sm text-muted-foreground">
        Your message goes to the support team.
      </p>
    </div>
  `,
})
export class TextareaDemoExample {}

A textarea takes longer text than an input: a comment, a message, a bio. ndTextarea styles a native textarea and grows with its content. It reflects the bound field's invalid and disabled states like Input does, so forms bind the textarea directly.

Installation

npx @ndop/cli add textarea

Usage

import { Label } from '@/ui/label/label';
import { Textarea } from '@/ui/textarea/textarea';
<div class="grid w-full max-w-sm gap-3">
  <label ndLabel for="textarea-demo-message">Your message</label>
  <textarea
    ndTextarea
    id="textarea-demo-message"
    placeholder="Type your message here."
    aria-describedby="textarea-demo-hint"
  ></textarea>
  <p id="textarea-demo-hint" class="text-sm text-muted-foreground">
    Your message goes to the support team.
  </p>
</div>

Examples

Textarea form

A required message field in a Signal Forms form, with its error.

160 characters left.

import { Component, signal } from '@angular/core';
import { form, FormField, maxLength, required } from '@angular/forms/signals';
import { Button } from '@/ui/button/button';
import { Label } from '@/ui/label/label';
import { Textarea } from '@/ui/textarea/textarea';

/**
 * A required message field in a Signal Forms form. It turns invalid once
 * touched, says why under the field, and counts what's left.
 */
@Component({
  selector: 'example-textarea-form',
  imports: [Button, FormField, Label, Textarea],
  template: `
    <form class="grid w-full max-w-sm gap-3" (submit)="send($event)">
      <label ndLabel for="textarea-form-bio">Bio</label>
      <textarea
        ndTextarea
        id="textarea-form-bio"
        placeholder="Tell us a little about yourself."
        aria-describedby="textarea-form-hint"
        [formField]="profile.bio"
      ></textarea>
      <p id="textarea-form-hint" class="text-sm text-muted-foreground">
        @if (profile.bio().touched() && profile.bio().invalid()) {
          Write a bio of up to 160 characters.
        } @else {
          {{ 160 - model().bio.length }} characters left.
        }
      </p>
      <button ndButton type="submit" class="justify-self-start">Save</button>
    </form>
  `,
})
export class TextareaFormExample {
  protected readonly model = signal({ bio: '' });

  protected readonly profile = form(this.model, (path) => {
    required(path.bio);
    maxLength(path.bio, 160);
  });

  /**
   * Marks the fields touched, so errors show, instead of reloading.
   *
   * @param event - The form's submit event.
   */
  protected send(event: Event): void {
    event.preventDefault();
    this.profile().markAsTouched();
  }
}

Textarea disabled

A disabled textarea with its label.

import { Component } from '@angular/core';
import { Label } from '@/ui/label/label';
import { Textarea } from '@/ui/textarea/textarea';

/** A disabled textarea; its label dims with it. */
@Component({
  selector: 'example-textarea-disabled',
  imports: [Label, Textarea],
  template: `
    <div class="grid w-full max-w-sm gap-3">
      <label ndLabel for="textarea-disabled-notes">Notes</label>
      <textarea ndTextarea id="textarea-disabled-notes" disabled>
Archived projects can't be edited.</textarea
      >
    </div>
  `,
})
export class TextareaDisabledExample {}

Accessibility

Focus

  • Unchanged from the native input. Focusing it and leaving it marks the bound field touched, through the forms library.

Right-to-left

Text direction follows the document or the element's dir; Ndop changes nothing.

API reference

Textarea

textarea[ndTextarea]role native

Styled multi-line text field (@ndop/primitives/input). It grows with its content (field-sizing: content) from a two-line minimum. Forms bind the native textarea; invalid and disabled states show once the bound field reports them, and a label[ndLabel] with a matching for follows them too.

Manages aria-invalid, aria-describedby.

Textarea binding Type Default
[class]
Extra classes, merged last so they win.
string''
[id]
Element id; also the key a label[ndLabel] uses to find this input.
string | undefined—
[invalid]
Marks the input invalid when no forms library is bound; with one, the field's state decides.
booleanfalse
Data attribute Values
data-slottextarea
data-disabled —
data-invalid —

Agent notes

Do

  • Give it a visible label[ndLabel] with a matching for and id.
  • Bind the form field to the textarea itself, with [formField], formControlName or ngModel.
  • Say the expected length or format in a description under it, linked with aria-describedby.

Don't

  • Don't use it for a single line of text; use Input.
  • Don't rely on the placeholder as the label; it disappears as soon as the user types.