Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

Components

Textarea

An input for multi-line text data. Basic textareas with labels, supporting text, and validation states.

Full support Supported since v125. Partial support Missing: field-sizing. Full support Supported since v26.2.

Variants

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Default" placeholder="Placeholder" />
<Textarea label="Filled" placeholder="Placeholder" filled />
<label class="textarea">
<span class="label">Default</span>
<span class="field">
<textarea id="textarea-1" placeholder="Placeholder"></textarea>
</span>
</label>
<label class="textarea filled">
<span class="label">Filled</span>
<span class="field">
<textarea id="textarea-2" placeholder="Placeholder"></textarea>
</span>
</label>

Sizes

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Small outlined" placeholder="Placeholder" small />
<Textarea label="Small filled" placeholder="Placeholder" small filled />
<label class="textarea small">
<span class="label">Small outlined</span>
<span class="field">
<textarea id="textarea-3" placeholder="Placeholder"></textarea>
</span>
</label>
<label class="textarea filled small">
<span class="label">Small filled</span>
<span class="field">
<textarea id="textarea-4" placeholder="Placeholder"></textarea>
</span>
</label>

End text

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Label" placeholder="Default" endText="Supporting text" />
<Textarea label="Label" placeholder="Filled" endText="Supporting text" filled />
<label class="textarea">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-5" placeholder="Default"></textarea>
</span>
<span class="end-text">Supporting text</span>
</label>
<label class="textarea filled">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-6" placeholder="Filled"></textarea>
</span>
<span class="end-text">Supporting text</span>
</label>

Affix

Use the prefix, suffix, header, and footer slots to affix content inside the textarea's border. Header and footer are particularly useful for filenames and character counters.

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Notes" placeholder="Add a note...">
<svg
slot="prefix"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</Textarea>
<label class="textarea">
<span class="label">Notes</span>
<span class="field">
<textarea id="textarea-7" placeholder="Add a note..."></textarea>
<span class="prefix"
><svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
></path></svg
></span>
</span>
</label>

Headers and footers

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Code" placeholder="console.log('Hello, world!')">
<Fragment slot="header">script.js</Fragment>
</Textarea>
<Textarea label="Comment" placeholder="Write a comment...">
<Fragment slot="footer">0 / 280</Fragment>
</Textarea>
<label class="textarea">
<span class="label">Code</span>
<span class="field">
<textarea
id="textarea-8"
placeholder="console.log('Hello, world!')"
></textarea>
<span class="header">script.js</span>
</span>
</label>
<label class="textarea">
<span class="label">Comment</span>
<span class="field">
<textarea id="textarea-9" placeholder="Write a comment..."></textarea>
<span class="footer">0 / 280</span>
</span>
</label>

Validation

Add [required] to the <textarea> element to toggle required styles

The critical prop toggles the error styles. Make use of the end text to give extra feedback on the error.

---
import { Textarea } from "@opui/astro"
---
<div class="example-row">
<Textarea label="Label" placeholder="Default" required />
<Textarea label="Label" placeholder="Filled" required filled />
</div>
<div class="example-row">
<Textarea
label="Label"
placeholder="Default"
endText="Only double-negatives are allowed."
critical
/>
<Textarea
label="Label"
placeholder="Filled"
endText="Only letters from the first half of the alphabet are allowed."
critical
filled
/>
</div>
<div class="example-row">
<label class="textarea">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-10" placeholder="Default" required></textarea>
</span>
</label>
<label class="textarea filled">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-11" placeholder="Filled" required></textarea>
</span>
</label>
</div>
<div class="example-row">
<label class="textarea" data-invalid="true">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-12" placeholder="Default"></textarea>
</span>
<span class="end-text">Only double-negatives are allowed.</span>
</label>
<label class="textarea filled" data-invalid="true">
<span class="label">Label</span>
<span class="field">
<textarea id="textarea-13" placeholder="Filled"></textarea>
</span>
<span class="end-text"
>Only letters from the first half of the alphabet are allowed.</span
>
</label>
</div>

Spread

Use the spread boolean prop to display the label and description on the left with the textarea on the right. The layout collapses to a column on narrow containers.

---
import { Textarea } from "@opui/astro"
---
<Textarea spread placeholder="Hello, world!">
<Fragment slot="label">Message</Fragment>
<Fragment slot="description"
>You can write your message here. Keep it short, preferably under 100
characters.</Fragment
>
</Textarea>
<Textarea spread placeholder="Additional notes..." filled>
<Fragment slot="label">Notes</Fragment>
<Fragment slot="description">Add any additional notes or comments</Fragment>
<Fragment slot="end-text">Maximum 500 characters</Fragment>
</Textarea>
<Textarea spread required label="Required">
<Fragment slot="description">You must provide a response</Fragment>
</Textarea>
<Textarea spread disabled label="Disabled">
<Fragment slot="description">This textarea is disabled</Fragment>
</Textarea>
<Textarea spread critical label="Invalid Message">
<Fragment slot="description">This textarea has an error</Fragment>
<Fragment slot="end-text">This value is too short.</Fragment>
</Textarea>
<Textarea spread label="Bio" placeholder="Tell us about yourself...">
<Fragment slot="description">Shown on your public profile</Fragment>
<Fragment slot="prefix">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</Fragment>
<Fragment slot="footer">280 characters left</Fragment>
</Textarea>
<Textarea
spread
filled
label="Release notes"
placeholder="Markdown supported..."
>
<Fragment slot="description">Shown on the changelog page</Fragment>
<Fragment slot="header">v1.4.0</Fragment>
<Fragment slot="footer">Saved 2 minutes ago</Fragment>
<Fragment slot="end-text">Drafts are auto-saved</Fragment>
</Textarea>
<label class="textarea spread">
<span class="label">Message</span>
<span class="start-text"
>You can write your message here. Keep it short, preferably under 100
characters.</span
>
<span class="field">
<textarea id="textarea-14" placeholder="Hello, world!"></textarea>
</span>
</label>
<label class="textarea filled spread">
<span class="label">Notes</span>
<span class="start-text">Add any additional notes or comments</span>
<span class="field">
<textarea id="textarea-15" placeholder="Additional notes..."></textarea>
</span>
<span class="end-text">Maximum 500 characters</span>
</label>
<label class="textarea spread">
<span class="label">Required</span>
<span class="start-text">You must provide a response</span>
<span class="field"><textarea id="textarea-16" required></textarea></span>
</label>
<label class="textarea spread">
<span class="label">Disabled</span>
<span class="start-text">This textarea is disabled</span>
<span class="field"><textarea disabled id="textarea-17"></textarea></span>
</label>
<label class="textarea spread" data-invalid="true">
<span class="label">Invalid Message</span>
<span class="start-text">This textarea has an error</span>
<span class="field"><textarea id="textarea-18"></textarea></span>
<span class="end-text">This value is too short.</span>
</label>
<label class="textarea spread">
<span class="label">Bio</span>
<span class="start-text">Shown on your public profile</span>
<span class="field">
<textarea
id="textarea-19"
placeholder="Tell us about yourself..."
></textarea>
<span class="prefix">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
></path>
<path
d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"
></path>
</svg>
</span>
<span class="footer">280 characters left</span>
</span>
</label>
<label class="textarea filled spread">
<span class="label">Release notes</span>
<span class="start-text">Shown on the changelog page</span>
<span class="field">
<textarea id="textarea-20" placeholder="Markdown supported..."></textarea>
<span class="header">v1.4.0</span>
<span class="footer">Saved 2 minutes ago</span>
</span>
<span class="end-text">Drafts are auto-saved</span>
</label>

Auto-fit

When enabled the Field changes size depending on its content.

---
import { Textarea } from "@opui/astro"
---
<Textarea label="Auto-fit" placeholder="Auto-fit" autoFit />
<label class="textarea auto-fit">
<span class="label">Auto-fit</span>
<span class="field">
<textarea id="textarea-21" placeholder="Auto-fit"></textarea>
</span>
</label>

Anatomy

  1. label.textarea: Container element
  2. .label: Field label element
  3. .field: The boxed input area
  4. .header: Optional inside-border header strip (with divider)
  5. .prefix: Optional inline-start affix
  6. <textarea>: Textarea element
  7. .suffix: Optional inline-end affix
  8. .footer: Optional inside-border footer strip (with divider)
  9. .end-text: Supporting text element

API

Text field API

Prop Type Default Description
autoFit boolean false Automatically adjusts its width to content.
description string - Description text displayed above the field.
critical boolean false Field error state. Sets [data-invalid] on the root element.
label string - The label for the field.
size "small" - The size of the field.
spread boolean false Spreads the label/description and input to opposite ends.
endText string - Supporting text displayed below the field.
variant "filled" - The visual variant of the field.

Slots

Slot Description
label Slot for the label element.
description Slot for the description (start text) element, displayed above the field.
prefix Content placed at the inline-start of the field, inside the border.
suffix Content placed at the inline-end of the field, inside the border.
header Content placed above the input, inside the border, with a divider.
footer Content placed below the input, inside the border, with a divider.
end-text Slot for the supporting text (end text) element.
supporting-text Legacy slot for the supporting text element.

Textarea API

Prop Type Default Description
autoFit boolean false Automatically adjusts its height to content.
description string - Description text displayed above the textarea.
endText string - Supporting text displayed below the textarea.
spread boolean false Spreads the label/description and textarea to opposite ends.
variant "filled" - The visual variant of the textarea.

Slots

Slot Description
label Slot for the label element.
description Slot for the description (start text) element, displayed above the field.
prefix Content placed at the inline-start of the field, inside the border.
suffix Content placed at the inline-end of the field, inside the border.
header Content placed above the textarea, inside the border, with a divider.
footer Content placed below the textarea, inside the border, with a divider.
end-text Slot for the supporting text (end text) element.
supporting-text Legacy slot for the supporting text element.

Browser support

Full support Supported since v125. Partial support Missing: field-sizing. Full support Supported since v26.2.

See also the full browser support guide.

Installation

Dependencies

@layer components.extended {
/*
- Common styling for input, textarea and select
- Form related styles such as: label, supporting text, error handling
*/
:where(.text-field, .textarea, .select) {
--_accent-color: var(--primary);
--_bg-color: var(--surface-default);
--_border-color: var(--field-border-color);
--_field-padding-block: var(--size-2);
--_field-padding-inline: var(--size-2);
--_filled-border-color: var(--text-primary);
--_height: var(--field-size);
--_label-color: var(--text-muted);
--_end-text-color: var(--text-muted);
display: grid;
gap: var(--size-1) 0;
position: relative;
/* Field - the actual input with a border around it */
.field {
background-color: var(--_bg-color);
border: var(--field-border-width) solid var(--_border-color);
border-radius: var(--field-border-radius);
display: grid;
grid-column: 1/-1;
grid-row: 2;
grid-template-columns: auto 1fr auto;
grid-template-areas:
"header header header"
"prefix input suffix"
"footer footer footer";
min-block-size: var(--_height);
@media (prefers-reduced-motion: no-preference) {
transition: border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
}
:where(.field) :where(input, textarea, select) {
background: transparent;
border: 0;
color: var(--text-primary);
font-family: var(--font-sans);
font-size: var(--font-size-1);
grid-area: input;
inline-size: 100%;
line-height: var(--font-lineheight-1);
min-inline-size: 0;
padding: var(--_field-padding-block) var(--_field-padding-inline);
&:focus,
&:focus-visible {
outline: 0;
}
}
/* Affixes */
.prefix,
.suffix {
align-items: center;
color: var(--_label-color);
display: inline-flex;
padding-inline: var(--_field-padding-inline);
white-space: nowrap;
}
.prefix {
grid-area: prefix;
}
.suffix {
grid-area: suffix;
}
.header,
.footer {
align-items: center;
color: var(--_label-color);
display: flex;
font-size: var(--font-size-0);
gap: var(--size-2);
padding: var(--size-1) var(--_field-padding-inline);
}
.header {
border-block-end: 1px solid var(--_border-color);
grid-area: header;
}
.footer {
border-block-start: 1px solid var(--_border-color);
grid-area: footer;
}
/* Remove padding on the input when affixes are next to it */
.field:has(> .prefix) :where(input, textarea, select) {
padding-inline-start: 0;
}
.field:has(> .suffix) :where(input, textarea, select) {
padding-inline-end: 0;
}
/* Textarea - keep affix aligned with the first line */
&.textarea .field {
.prefix,
.suffix {
align-self: start;
padding-block: var(--_field-padding-block);
}
}
/* Required/Invalid */
&:has(:invalid) {
.label:after {
color: var(--red);
content: "*";
margin: -0.25em auto auto 0.25em;
}
}
/* File */
&:has(input[type="file"]) {
cursor: pointer;
input {
align-self: flex-start;
block-size: var(--_height);
box-shadow: none;
color: var(--text-primary);
cursor: inherit;
max-inline-size: 100%;
padding: 0;
transition: font-size 0.2s var(--ease-3);
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-tonal);
block-size: calc(100% - var(--size-2) * 2);
border: none;
border-radius: var(--field-border-radius);
cursor: pointer;
margin-block-start: var(--size-2);
margin-inline-end: 1ex;
margin-inline-start: var(--size-2);
}
}
/* Variants */
&.filled {
input {
&::-webkit-file-upload-button,
&::file-selector-button {
background-color: var(--surface-default);
block-size: calc(100% - var(--size-2) * 2);
border-radius: var(--field-border-radius);
cursor: pointer;
margin-block-start: var(--size-2);
}
}
}
/* Sizes */
&.small {
input {
font-size: var(--font-size-05);
&::-webkit-file-upload-button,
&::file-selector-button {
block-size: calc(100% - var(--size-2));
margin-block-start: var(--size-1);
}
}
}
}
/* Autosuggest */
&:has(input[list]) {
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Select */
&:has(select) {
.label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Customizable Select */
&:has(select button) {
select {
padding: 0;
button {
outline: 0;
padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline);
}
}
}
/* Non-customizable Select */
&:has(select):not(:has(button)) {
select {
padding: var(--_field-padding-block) var(--size-8) var(--_field-padding-block) var(--_field-padding-inline);
}
}
/* Input - color */
&:has(input[type="color"]) {
.field {
block-size: var(--_height);
inline-size: var(--_height);
min-block-size: 0;
overflow: hidden;
}
input {
appearance: none;
background: none;
block-size: var(--_height);
inline-size: var(--_height);
overflow: hidden;
padding: 0;
&::-webkit-color-swatch {
border: none;
}
&::-webkit-color-swatch-wrapper {
padding: 0;
}
}
}
/* Element states */
&:hover {
&:not([data-invalid]) {
.field {
border-color: var(--text-primary);
}
}
}
&:focus-within {
.field {
border-color: var(--_accent-color);
outline-offset: -2px;
}
}
/* Label */
.label {
color: var(--text-primary);
font-size: var(--font-size-05);
font-weight: 600;
grid-column: 1/-1;
grid-row: 1;
padding-inline: 0 1ex;
}
/* End text */
:where(.end-text) {
color: var(--_end-text-color);
font-size: var(--font-size-0);
grid-column: 1/-1;
grid-row: 3;
line-height: 1.5;
}
/* Auto-fit */
&.auto-fit {
inline-size: auto;
.field {
inline-size: auto;
}
:where(& input, & textarea) {
field-sizing: content;
inline-size: auto;
min-inline-size: 25ch;
}
}
/* Validation */
&[data-invalid],
&:has(:user-invalid) {
--_accent-color: var(--color-9);
--_border-color: var(--color-9);
--_filled-border-color: var(--color-9);
--_label-color: var(--color-9);
--_end-text-color: var(--color-9);
}
/*
* Variant: Filled
*/
&.filled {
--_bg-color: var(--surface-tonal);
&:not(:has([disabled], :has(input[type="color"]))) {
/* Hover */
&:hover {
--_bg-color: light-dark(oklch(from var(--surface-tonal) calc(l * 0.93) c h),
oklch(from var(--surface-tonal) calc(l * 1.1) c h));
}
}
}
/* Disabled */
&:where(:has([disabled])) {
cursor: not-allowed;
opacity: 0.64;
user-select: none;
&::before {
display: none;
}
.field {
cursor: not-allowed;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
.label,
.start-text,
.end-text {
cursor: not-allowed;
}
}
/* Read-only */
&:where(:has([readonly])) {
&::before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
}
/* Sizes */
&.small {
--_field-padding-block: var(--size-2);
--_height: var(--field-size-small);
&:has(input[type="color"]) {
.label {
line-height: 1.5;
}
}
}
/* Orientation */
&.spread {
align-items: start;
column-gap: var(--size-4);
container-type: inline-size;
grid-template-columns: 1fr auto;
.label {
font-weight: 600;
grid-column: 1;
grid-row: 1;
}
.start-text {
color: var(--_end-text-color);
font-size: var(--font-size-0);
grid-column: 1;
grid-row: 2;
line-height: 1.5;
}
.field {
align-self: start;
grid-column: 2;
grid-row: 1 / span 2;
}
.field:has(textarea) {
grid-row: 1 / span 3;
+.end-text {
grid-row: 4;
}
}
:where(textarea) {
min-inline-size: 30ch;
}
:where(.end-text) {
grid-column: 2;
text-align: end;
}
/* Collapse to column on narrow containers */
@container (width < 400px) {
column-gap: 0;
grid-template-columns: 1fr;
.label {
grid-column: 1;
grid-row: 1;
}
.start-text {
grid-column: 1;
grid-row: 2;
}
.field,
.field:has(textarea) {
grid-column: 1;
grid-row: 3;
}
:where(.end-text) {
grid-column: 1;
grid-row: 4;
text-align: start;
}
}
}
}
}
@layer components.extended {
:where(.textarea) {
textarea {
block-size: auto;
field-sizing: content;
min-block-size: calc(var(--_field-padding-block) * 2 + (var(--border-width) * 2) + 3lh);
resize: vertical;
}
/* Size */
&.small {
textarea {
min-block-size: var(--size-9);
}
}
/* Auto-fit */
&.auto-fit {
textarea {
resize: both;
}
}
}
}