Components
Textarea
Full support Supported since v125. Partial support
Missing:
field-sizing.
Full support Supported since v26.2. Variants
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Default" placeholder="Placeholder" /> <Textarea label="Filled" placeholder="Placeholder" filled /></template><!--[--><label class="ui-textarea" ><span class="ui-label"><!--[-->Default<!--]--></span ><!----><span class="ui-field"> <textarea id="s0-0" placeholder="Placeholder"></textarea ><!----><!----><!----><!----></span ><!----><!--[--><!--]--></label><label class="ui-textarea ui-filled" ><span class="ui-label"><!--[-->Filled<!--]--></span ><!----><span class="ui-field"> <textarea id="s0-1" placeholder="Placeholder"></textarea ><!----><!----><!----><!----></span ><!----><!--[--><!--]--></label><!--]-->Sizes
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Small outlined" placeholder="Placeholder" small /> <Textarea label="Small filled" placeholder="Placeholder" small filled /></template><!--[--><label class="ui-textarea ui-small" ><span class="ui-label"><!--[-->Small outlined<!--]--></span ><!----><span class="ui-field"> <textarea id="s1-0" 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="s1-1" placeholder="Placeholder"></textarea ><!----><!----><!----><!----></span ><!----><!--[--><!--]--></label><!--]-->End text
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Label" placeholder="Default" endText="Supporting text" /> <Textarea label="Label" placeholder="Filled" endText="Supporting text" filled /></template><!--[--><label class="ui-textarea" ><span class="ui-label"><!--[-->Label<!--]--></span ><!----><span class="ui-field"> <textarea id="s2-0" 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="s2-1" 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.
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Notes" placeholder="Add a note..."> <template #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 ></template> </Textarea></template><label class="ui-textarea" ><span class="ui-label"><!--[-->Notes<!--]--></span ><!----><span class="ui-field"> <textarea id="s3-0" 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
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Code" placeholder="console.log('Hello, world!')"> <template #header>script.js</template> </Textarea>
<Textarea label="Comment" placeholder="Write a comment..."> <template #footer>0 / 280</template> </Textarea></template><!--[--><label class="ui-textarea" ><span class="ui-label"><!--[-->Code<!--]--></span ><!----><span class="ui-field"> <textarea id="s4-0" 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="s4-1" placeholder="Write a comment..."></textarea ><!----><!----><!----><span class="ui-footer" ><!--[-->0 / 280<!--]--></span ></span ><!----><!--[--><!--]--></label><!--]-->Validation
Add the required attribute on the component. It is forwarded
to the underlying <textarea>.
Use the error prop to toggle invalid styles. It renders
data-invalid on the root element. Make use of the end text to
give extra feedback on the error.
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <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></template><!--[--><div class="example-row"> <label class="ui-textarea" ><span class="ui-label"><!--[-->Label<!--]--></span ><!----><span class="ui-field"> <textarea id="s5-0" placeholder="Default" required></textarea ><!----><!----><!----><!----></span ><!----><!--[--><!--]--></label ><label class="ui-textarea ui-filled" ><span class="ui-label"><!--[-->Label<!--]--></span ><!----><span class="ui-field"> <textarea id="s5-1" 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="s5-2" 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="s5-3" 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.
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea spread placeholder="Hello, world!"> <template #label>Message</template> <template #description >You can write your message here. Keep it short, preferably under 100 characters.</template > </Textarea>
<Textarea spread placeholder="Additional notes..." filled> <template #label>Notes</template> <template #description>Add any additional notes or comments</template> <template #end-text>Maximum 500 characters</template> </Textarea>
<Textarea spread required label="Required"> <template #description>You must provide a response</template> </Textarea>
<Textarea spread disabled label="Disabled"> <template #description>This textarea is disabled</template> </Textarea>
<Textarea spread error label="Invalid Message"> <template #description>This textarea has an error</template> <template #end-text>This value is too short.</template> </Textarea>
<Textarea spread label="Bio" placeholder="Tell us about yourself..."> <template #description>Shown on your public profile</template> <template #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> </template> <template #footer>280 characters left</template> </Textarea>
<Textarea spread filled label="Release notes" placeholder="Markdown supported..." > <template #description>Shown on the changelog page</template> <template #header>v1.4.0</template> <template #footer>Saved 2 minutes ago</template> <template #end-text>Drafts are auto-saved</template> </Textarea></template><!--[--><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="s6-0" 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="s6-1" 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="s6-2" 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 id="s6-3" disabled></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="s6-4"></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="s6-5" 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="s6-6" 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.
<script setup lang="ts">import { Textarea } from "opui-css/vue"</script>
<template> <Textarea label="Auto-fit" placeholder="Auto-fit" autoFit /></template><label class="ui-textarea ui-auto-fit" ><span class="ui-label"><!--[-->Auto-fit<!--]--></span ><!----><span class="ui-field"> <textarea id="s7-0" placeholder="Auto-fit"></textarea ><!----><!----><!----><!----></span ><!----><!--[--><!--]--></label>Anatomy
label.ui-textarea: Container element.ui-label: Field label element.ui-field: The boxed input area-
.ui-header: Optional inside-border header strip (with divider) .ui-prefix: Optional inline-start affix<textarea>: Textarea element.ui-suffix: Optional inline-end affix-
.ui-footer: Optional inside-border footer strip (with divider) .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; } } }}