Textarea
<gui-textarea> is a multi-line text field. It renders a native <textarea> with its label, hint
and errors, and can count the characters against a limit.
import { GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function TextareaBasic() { return <GuiTextarea label="Message" />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/textarea';
@Component({ selector: 'app-textarea-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<gui-textarea label="Message"></gui-textarea>`,})export class TextareaBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';</script>
<template> <gui-textarea label="Message"></gui-textarea></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('textarea-basic')export class TextareaBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-textarea label="Message"></gui-textarea> `; }}<gui-textarea label="Message"></gui-textarea>The element’s entry point is @golemui/gui-components/textarea, which registers <gui-textarea>
and nothing else. To install the package and load the stylesheet, see
Installation.
The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.
import { useState } from 'react';import { GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function MessageField() { const [message, setMessage] = useState('');
return ( <GuiTextarea label="Message" name="message" value={message} onGuiChange={(event) => setMessage(event.detail.value ?? '')} /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiEventMap, GuiTextareaEvents } from '@golemui/gui-components';import '@golemui/gui-components/textarea';
type ChangeEvent = GuiEventMap<typeof GuiTextareaEvents>['gui-change'];
@Component({ selector: 'app-message-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-textarea label="Message" name="message" [value]="message" (gui-change)="onChange($event)" ></gui-textarea> `,})export class MessageFieldComponent { message = '';
onChange(event: Event) { this.message = (event as ChangeEvent).detail.value ?? ''; }}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
const message = ref('');</script>
<template> <gui-textarea label="Message" name="message" :value="message" @gui-change="message = $event.detail.value ?? ''" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('message-field')export class MessageField extends LitElement { @state() private message = '';
protected override createRenderRoot() { return this; }
override render() { return html` <gui-textarea label="Message" name="message" .value=${this.message} @gui-change=${(event: CustomEvent) => (this.message = event.detail.value)} ></gui-textarea> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-textarea label="Message" name="message"></gui-textarea>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-textarea');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});Properties
| Property | Type | Default | Description |
|---|---|---|---|
localeId
attribute locale-id | string | 'en' | BCP 47 locale for formatting and parsing, such as en-US or es. |
value | string | – | The text. |
placeholder | string | – | Text shown while the control is empty. |
autocomplete | string | – | The autocomplete hint passed to the inner native control. |
counterMode
attribute counter-mode | 'remaining' | 'current' | – | With maxLength, whether the counter shows the characters left (remaining) or used (current). |
minimumHeight
attribute minimum-height | number | – | Minimum height of the field, in pixels. |
autoGrow
attribute auto-grow | boolean | false | Grows the field with its content instead of scrolling. |
maxLength
attribute maxlength | number | – | The number of characters the counter counts against. It only drives the counter: longer text is not blocked, and the counter marks it as over the limit. |
uid | string | – | The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration. |
Form control properties
Every field has these, from the base class of the form controls.
| Property | Type | Default | Description |
|---|---|---|---|
name | string | – | The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute. |
label | string | – | The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds. |
hint | string | – | Help text shown under the label and announced as the control's description. |
errors | string[] | [] | Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false. |
touched | boolean | – | Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true. |
required | boolean | false | The control needs a value: it is marked as required and, with a name, blocks submission. |
readOnly
attribute readonly | boolean | – | The value can be read and focused but not changed. |
disabled | boolean | – | Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's. |
form read-only, no attribute | HTMLFormElement | null | – | The <form> the element belongs to, if any. |
validity read-only, no attribute | ValidityState | – | The element's validity, like a native control's. |
validationMessage read-only, no attribute | string | – | The message the browser shows for an invalid value. |
Methods
| Method | Description |
|---|---|
checkValidity(): boolean | – |
reportValidity(): boolean | – |
Events
Every event bubbles and is composed, so you can listen on any ancestor.
| Event | Description |
|---|---|
gui-input | The user changed the value. detail.value is the new value. |
gui-change | The user committed the text, on blur. detail.value is the value. |
gui-blur | Focus left the control. |
Examples
Section titled “Examples”Character counter
Section titled “Character counter”maxlength shows a counter under the field. By default it counts the characters left; with
counter-mode="current" it counts the characters used. The limit only drives the counter: the
user can type past it, and the counter turns to the error color and goes below zero.
import { GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function TextareaCounter() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiTextarea label="Status" maxLength={140} value="Packing for Lisbon." /> </div> <div className="gui-grid__cell"> <GuiTextarea label="Bio" maxLength={160} counterMode="current" value="Mathematician. Writes notes longer than the paper they annotate." /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/textarea';
@Component({ selector: 'app-textarea-counter', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Status" maxlength="140" value="Packing for Lisbon." ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Bio" maxlength="160" counter-mode="current" value="Mathematician. Writes notes longer than the paper they annotate." ></gui-textarea> </div> </div> `,})export class TextareaCounterComponent {}<script setup lang="ts">import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Status" :maxLength="140" value="Packing for Lisbon." ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Bio" :maxLength="160" counter-mode="current" value="Mathematician. Writes notes longer than the paper they annotate." ></gui-textarea> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('textarea-counter')export class TextareaCounter extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Status" maxlength="140" value="Packing for Lisbon." ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Bio" maxlength="160" counter-mode="current" value="Mathematician. Writes notes longer than the paper they annotate." ></gui-textarea> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Status" maxlength="140" value="Packing for Lisbon."></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Bio" maxlength="160" counter-mode="current" value="Mathematician. Writes notes longer than the paper they annotate." ></gui-textarea> </div></div>To stop a longer text, check its length in your own validation and set errors.
Height and auto-grow
Section titled “Height and auto-grow”The field starts 120 pixels tall; minimum-height changes that, in pixels. The user can drag its
corner to make it taller. With auto-grow, the field grows with its text instead of scrolling,
and never gets shorter than minimum-height. Type a few lines in both fields to compare.
import { GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function TextareaHeight() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiTextarea label="Short note" minimumHeight={60} /> </div> <div className="gui-grid__cell"> <GuiTextarea label="Meeting notes" minimumHeight={60} autoGrow hint="Grows as you type." /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/textarea';
@Component({ selector: 'app-textarea-height', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Short note" minimum-height="60"></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Meeting notes" minimum-height="60" auto-grow hint="Grows as you type." ></gui-textarea> </div> </div> `,})export class TextareaHeightComponent {}<script setup lang="ts">import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Short note" :minimumHeight="60"></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Meeting notes" :minimumHeight="60" :autoGrow="true" hint="Grows as you type." ></gui-textarea> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('textarea-height')export class TextareaHeight extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Short note" minimum-height="60"></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Meeting notes" minimum-height="60" auto-grow hint="Grows as you type." ></gui-textarea> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Short note" minimum-height="60"></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Meeting notes" minimum-height="60" auto-grow hint="Grows as you type." ></gui-textarea> </div></div>Disabled and read-only
Section titled “Disabled and read-only”A disabled field can’t be focused or changed, and a form doesn’t submit it. A readonly field
can be focused, selected and copied, and a form submits its value.
import { GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function TextareaDisabledReadonly() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiTextarea label="Delivery notes" value="Leave it with the concierge." disabled /> </div> <div className="gui-grid__cell"> <GuiTextarea label="Terms" value="Plans renew every month. Cancel any time from Settings." readOnly /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/textarea';
@Component({ selector: 'app-textarea-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Delivery notes" value="Leave it with the concierge." disabled ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Terms" value="Plans renew every month. Cancel any time from Settings." readonly ></gui-textarea> </div> </div> `,})export class TextareaDisabledReadonlyComponent {}<script setup lang="ts">import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Delivery notes" value="Leave it with the concierge." :disabled="true" ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Terms" value="Plans renew every month. Cancel any time from Settings." :readOnly="true" ></gui-textarea> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('textarea-disabled-readonly')export class TextareaDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Delivery notes" value="Leave it with the concierge." disabled ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Terms" value="Plans renew every month. Cancel any time from Settings." readonly ></gui-textarea> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-textarea label="Delivery notes" value="Leave it with the concierge." disabled ></gui-textarea> </div> <div class="gui-grid__cell"> <gui-textarea label="Terms" value="Plans renew every month. Cancel any time from Settings." readonly ></gui-textarea> </div></div>placeholder, hint and errors work as on the
text input.
In a form
Section titled “In a form”With a name, <gui-textarea> submits its text, line breaks included, under that name, and
required blocks an empty submit. maxlength doesn’t block a submit.
import { useState, type SubmitEvent } from 'react';import { GuiButton, GuiTextarea } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function TextareaNativeForm() { const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); setResult(`Submitted ${JSON.stringify(data)}`); };
return ( <form id="feedback" onSubmit={onSubmit}> <GuiTextarea label="What could we do better?" name="feedback" maxLength={500} required /> <GuiButton type="submit" label="Send feedback" /> <p id="feedback-result" role="status"> {result} </p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/textarea';
@Component({ selector: 'app-textarea-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="feedback" #form (submit)="onSubmit($event, form)"> <gui-textarea label="What could we do better?" name="feedback" maxlength="500" required ></gui-textarea> <gui-button type="submit" label="Send feedback"></gui-button> <p id="feedback-result" role="status">{{ result() }}</p> </form> `,})export class TextareaNativeFormComponent { result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); this.result.set(`Submitted ${JSON.stringify(data)}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const data = Object.fromEntries(new FormData(event.currentTarget)); result.value = `Submitted ${JSON.stringify(data)}`;}</script>
<template> <form id="feedback" @submit.prevent="onSubmit"> <gui-textarea label="What could we do better?" name="feedback" :maxLength="500" :required="true" ></gui-textarea> <gui-button type="submit" label="Send feedback"></gui-button> <p id="feedback-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import '@golemui/gui-components/button';import '@golemui/gui-components/textarea';import '@golemui/gui-components/index.css';
@customElement('textarea-native-form')export class TextareaNativeForm extends LitElement { @query('#feedback') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = Object.fromEntries(new FormData(this.form)); this.result = `Submitted ${JSON.stringify(data)}`; }
override render() { return html` <form id="feedback" @submit=${this.onSubmit}> <gui-textarea label="What could we do better?" name="feedback" maxlength="500" required ></gui-textarea> <gui-button type="submit" label="Send feedback"></gui-button> <p id="feedback-result" role="status">${this.result}</p> </form> `; }}<form id="feedback"> <gui-textarea label="What could we do better?" name="feedback" maxlength="500" required ></gui-textarea> <gui-button type="submit" label="Send feedback"></gui-button> <p id="feedback-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#feedback');
form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); document.querySelector('#feedback-result').textContent = `Submitted ${JSON.stringify(data)}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<gui-textarea> renders into the light DOM, so you can style every part with plain CSS. These
are the parts and their public classes:
<gui-textarea class="gui-field"> <label class="gui-label" for="bio" id="bio_label"> <span class="gui-label__text">Bio</span> <div class="gui-widget-hint" id="bio_hint" aria-hidden="true"> Shown on your profile. </div> </label> <div class="gui-widget"> <textarea id="bio" class="gui-widget-input" style="height: 120px; min-height: 120px;" aria-describedby="bio_hint" ></textarea> </div> <div class="gui-textarea--validation"> <div> <div class="gui-validator gui-validator--empty" id="bio_errors" role="alert" ></div> </div> <div class="gui-textarea--counter"> <span>96</span> <span> / 160</span> </div> </div></gui-textarea>| Class | Part |
|---|---|
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label | The label, with the text and the hint. |
gui-label__text | The label text and the required marker. |
gui-widget-hint | The hint. |
gui-widget | The box around the control. |
gui-widget-input | The <textarea>, the part that looks like a field. |
gui-textarea--validation | The row under the field, with the errors and the counter. |
gui-validator | The errors container. It is always there, so the alert role announces new errors. |
gui-validator--empty | On the errors container while there are no errors. |
gui-validator__error | One error message. |
gui-textarea--counter | The counter, with maxlength only. |
gui-textarea--counter__error | On the counter while the text is over the limit. |
The element sets the field’s height in an inline style, from minimum-height and, with
auto-grow, from the text. The ids derive from uid, or from a generated id such as
gui-textarea-3 when you don’t set one. Select by class or by tag, not by id.
Tokens
Section titled “Tokens”The field reads these design tokens. Set them on :root to change every field, or on any element
to change the fields inside it:
| Token | What it styles |
|---|---|
--gui-radius-md | The corner radius. |
--gui-bg-surface | The field’s background. |
--gui-border-default | The border. |
--gui-text-default | The text and the label. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label, the field and the counter. |
--gui-font-xs | The text size of the hint and the errors. |
--gui-text-secondary | The hint. |
--gui-space-2 | The field’s top and bottom padding, and the space over the counter. |
--gui-space-3 | The field’s side padding. |
--gui-border-focus | The border while focused. |
--gui-shadow-focus | The focus ring. |
--gui-intent-error | The border, the messages and the counter over the limit. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-bg-disabled | The background when disabled. |
--gui-text-disabled | The text when disabled. |
--gui-space-1 | The space between the label, hint and errors. |
To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.
Accessibility
Section titled “Accessibility”- Name. The
labelnames the field. Without one the field has no accessible name. - Description. The
hintis the field’s description, througharia-describedby. - Errors. While errors show, the textarea has
aria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear. - Required.
requiredsetsaria-required; the visual marker is hidden from screen readers. - Counter. The counter is visual only: it isn’t linked to the field and screen readers don’t
announce it. If the limit matters, say it in the
hint.
| Key | Action |
|---|---|
| Tab | Moves focus to the field, and away from it. |
| Enter | Adds a new line. Leaving the field commits the value and fires gui-change. |
| Every editing key | Works as in any native textarea. |
What every element shares, such as translating the browser messages, is in Accessibility.