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 textareapnpm dlx @ndop/cli add textareayarn dlx @ndop/cli add textareabunx @ndop/cli add textareaInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/textarea/textarea.tsimport { computed, Directive, input } from '@angular/core'; import { NdInput } from '@ndop/primitives/input'; import { cn } from '@/lib/utils'; /** * 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. * * @example * ```html * <label ndLabel for="bio">Bio</label> * <textarea ndTextarea id="bio" [formField]="form.bio"></textarea> * ``` */ @Directive({ selector: 'textarea[ndTextarea]', hostDirectives: [{ directive: NdInput, inputs: ['id', 'invalid'] }], host: { 'data-slot': 'textarea', '[class]': 'classes()' }, }) export class Textarea { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
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.
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 nativeStyled 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. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | textarea |
data-disabled | — |
data-invalid | — |
Agent notes
Do
- Give it a visible
label[ndLabel]with a matchingforandid. - Bind the form field to the
textareaitself, with[formField],formControlNameorngModel. - 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.