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

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="ui-textarea">
<span class="ui-label"> Default </span>
<span class="ui-field">
<textarea id="textarea-1" placeholder="Placeholder"></textarea>
</span>
</label>
<label class="ui-textarea ui-filled">
<span class="ui-label"> Filled </span>
<span class="ui-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="ui-textarea ui-small">
<span class="ui-label"> Small outlined </span>
<span class="ui-field">
<textarea id="textarea-3" placeholder="Placeholder"></textarea>
</span>
</label>
<label class="ui-textarea ui-filled ui-small">
<span class="ui-label"> Small filled </span>
<span class="ui-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="ui-textarea">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-5" placeholder="Default"></textarea>
</span>
<span class="ui-end-text"> Supporting text </span>
</label>
<label class="ui-textarea ui-filled">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-6" placeholder="Filled"></textarea>
</span>
<span class="ui-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="ui-textarea">
<span class="ui-label"> Notes </span>
<span class="ui-field">
<textarea id="textarea-7" placeholder="Add a note..."></textarea>
<span class="ui-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="ui-textarea">
<span class="ui-label"> Code </span>
<span class="ui-field">
<textarea
id="textarea-8"
placeholder="console.log('Hello, world!')"
></textarea>
<span class="ui-header"> script.js </span>
</span>
</label>
<label class="ui-textarea">
<span class="ui-label"> Comment </span>
<span class="ui-field">
<textarea id="textarea-9" placeholder="Write a comment..."></textarea>
<span class="ui-footer"> 0 / 280 </span>
</span>
</label>

Validation

Set required on the component to toggle required styles on the textarea.

Use the error prop to toggle invalid styles. It renders the data-invalid attribute on the root element. 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."
error
/>
<Textarea
label="Label"
placeholder="Filled"
endText="Only letters from the first half of the alphabet are allowed."
error
filled
/>
</div>
<div class="example-row">
<label class="ui-textarea">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-10" placeholder="Default" required></textarea>
</span>
</label>
<label class="ui-textarea ui-filled">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-11" placeholder="Filled" required></textarea>
</span>
</label>
</div>
<div class="example-row">
<label class="ui-textarea" data-invalid="true">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-12" placeholder="Default"></textarea>
</span>
<span class="ui-end-text"> Only double-negatives are allowed. </span>
</label>
<label class="ui-textarea ui-filled" data-invalid="true">
<span class="ui-label"> Label </span>
<span class="ui-field">
<textarea id="textarea-13" placeholder="Filled"></textarea>
</span>
<span class="ui-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 error 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="ui-textarea ui-spread">
<span class="ui-label"> Message </span>
<span class="ui-start-text">
You can write your message here. Keep it short, preferably under 100
characters.
</span>
<span class="ui-field">
<textarea id="textarea-14" placeholder="Hello, world!"></textarea>
</span>
</label>
<label class="ui-textarea ui-filled ui-spread">
<span class="ui-label"> Notes </span>
<span class="ui-start-text"> Add any additional notes or comments </span>
<span class="ui-field">
<textarea id="textarea-15" placeholder="Additional notes..."></textarea>
</span>
<span class="ui-end-text"> Maximum 500 characters </span>
</label>
<label class="ui-textarea ui-spread">
<span class="ui-label"> Required </span>
<span class="ui-start-text"> You must provide a response </span>
<span class="ui-field"><textarea id="textarea-16" required></textarea></span>
</label>
<label class="ui-textarea ui-spread">
<span class="ui-label"> Disabled </span>
<span class="ui-start-text"> This textarea is disabled </span>
<span class="ui-field"><textarea disabled id="textarea-17"></textarea></span>
</label>
<label class="ui-textarea ui-spread" data-invalid="true">
<span class="ui-label"> Invalid Message </span>
<span class="ui-start-text"> This textarea has an error </span>
<span class="ui-field"><textarea id="textarea-18"></textarea></span>
<span class="ui-end-text"> This value is too short. </span>
</label>
<label class="ui-textarea ui-spread">
<span class="ui-label"> Bio </span>
<span class="ui-start-text"> Shown on your public profile </span>
<span class="ui-field">
<textarea
id="textarea-19"
placeholder="Tell us about yourself..."
></textarea>
<span class="ui-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="ui-footer"> 280 characters left </span>
</span>
</label>
<label class="ui-textarea ui-filled ui-spread">
<span class="ui-label"> Release notes </span>
<span class="ui-start-text"> Shown on the changelog page </span>
<span class="ui-field">
<textarea id="textarea-20" placeholder="Markdown supported..."></textarea>
<span class="ui-header"> v1.4.0 </span>
<span class="ui-footer"> Saved 2 minutes ago </span>
</span>
<span class="ui-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="ui-textarea ui-auto-fit">
<span class="ui-label"> Auto-fit </span>
<span class="ui-field">
<textarea id="textarea-21" placeholder="Auto-fit"></textarea>
</span>
</label>

Anatomy

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

API

Text field API

Textarea API

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(.ui-text-field, .ui-textarea, .ui-select) {
--_motion: var(--motion, 1);
--_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 */
.ui-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);
transition: border-color calc(0.2s * var(--_motion))
cubic-bezier(0.4, 0, 0.2, 1);
}
:where(.ui-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 */
.ui-prefix,
.ui-suffix {
align-items: center;
color: var(--_label-color);
display: inline-flex;
padding-inline: var(--_field-padding-inline);
white-space: nowrap;
}
.ui-prefix {
grid-area: prefix;
}
.ui-suffix {
grid-area: suffix;
}
.ui-header,
.ui-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);
}
.ui-header {
border-block-end: 1px solid var(--_border-color);
grid-area: header;
}
.ui-footer {
border-block-start: 1px solid var(--_border-color);
grid-area: footer;
}
/* Remove padding on the input when affixes are next to it */
.ui-field:has(> .ui-prefix) :where(input, textarea, select) {
padding-inline-start: 0;
}
.ui-field:has(> .ui-suffix) :where(input, textarea, select) {
padding-inline-end: 0;
}
/* Textarea - keep affix aligned with the first line */
&.ui-textarea .ui-field {
.ui-prefix,
.ui-suffix {
align-self: start;
padding-block: var(--_field-padding-block);
}
}
/* Required/Invalid */
&:has(:invalid) {
.ui-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 */
&.ui-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 */
&.ui-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]) {
.ui-label {
/* Make sure chevron is visible */
inline-size: calc(100% - var(--size-6));
}
}
/* Select */
&:has(select) {
.ui-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"]) {
.ui-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]) {
.ui-field {
border-color: var(--text-primary);
}
}
}
&:focus-within {
.ui-field {
border-color: var(--_accent-color);
outline-offset: -2px;
}
}
/* Label */
.ui-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(.ui-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 */
&.ui-auto-fit {
inline-size: auto;
.ui-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
*/
&.ui-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;
}
.ui-field {
cursor: not-allowed;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
.ui-label,
.ui-start-text,
.ui-end-text {
cursor: not-allowed;
}
}
/* Read-only */
&:where(:has([readonly])) {
&::before {
display: none;
}
:where(input, textarea, select) {
cursor: not-allowed;
* {
pointer-events: none;
}
}
}
/* Sizes */
&.ui-small {
--_field-padding-block: var(--size-2);
--_height: var(--field-size-small);
&:has(input[type="color"]) {
.ui-label {
line-height: 1.5;
}
}
}
/* Orientation */
&.ui-spread {
align-items: start;
column-gap: var(--size-4);
container-type: inline-size;
grid-template-columns: 1fr auto;
.ui-label {
font-weight: 600;
grid-column: 1;
grid-row: 1;
}
.ui-start-text {
color: var(--_end-text-color);
font-size: var(--font-size-0);
grid-column: 1;
grid-row: 2;
line-height: 1.5;
}
.ui-field {
align-self: start;
grid-column: 2;
grid-row: 1 / span 2;
}
.ui-field:has(textarea) {
grid-row: 1 / span 3;
+ .ui-end-text {
grid-row: 4;
}
}
:where(textarea) {
min-inline-size: 30ch;
}
:where(.ui-end-text) {
grid-column: 2;
text-align: end;
}
/* Collapse to column on narrow containers */
@container (width < 400px) {
column-gap: 0;
grid-template-columns: 1fr;
.ui-label {
grid-column: 1;
grid-row: 1;
}
.ui-start-text {
grid-column: 1;
grid-row: 2;
}
.ui-field,
.ui-field:has(textarea) {
grid-column: 1;
grid-row: 3;
}
:where(.ui-end-text) {
grid-column: 1;
grid-row: 4;
text-align: start;
}
}
}
}
}
@layer components.extended {
:where(.ui-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 */
&.ui-small {
textarea {
min-block-size: var(--size-9);
}
}
/* Auto-fit */
&.ui-auto-fit {
textarea {
resize: both;
}
}
}
}