oriUI

Textarea

A labelled, accessible multiline text field with v-model. State is expressed through real attributes — the native disabled, aria-invalid when there is an error, and an aria-describedby that points at the rendered hint or error. The label is wired to the field with for/id (auto-generated via useId when you don't pass one). Unlike a single-line input the field has no fixed height — it grows from a rows-based min-height and stays user-resizable.

The examples are organised by layer: the class reference is the standalone @oriui/css layer, and the Framework API is the @oriui/vue component. Every example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte / plain-HTML usage), Vue; HTML is the default.

Classes

A textarea is a block wrapper plus single-class token utilities — one class repoints one token, no base class needed. The ori-color_* accent drives the focus ring; the idle border is a neutral, theme-aware blend. The Vue props in Framework API map 1:1 to these.

ClassTypeDescription
ori-textareaBlockRequired base class (wrapper div).
ori-textarea_*Styleoutline · fill
ori-color_*Colorprimary · secondary · success · warn · danger · info · surface · background (focus ring accent)
ori-textarea_* (size)Sizexs · sm · md · lg · xl · xxl (sets min-height on wrapper via --ori-size-action)
ori-size-radius_*Radiuszero · xs · sm · md · lg · xl · rounded (field corners)
ori-font-size_*Fontxs · sm · md · lg · xl · xxl (label + field text scale)
ori-textarea__label · ori-textarea__required · ori-textarea__field · ori-textarea__hint · ori-textarea__errorPartlabel / required-asterisk / textarea / helper / error elements
ori-textarea_fluidLayoutfull-width (stretches wrapper to 100 %)
disabled · aria-invalid · aria-describedbyStatereal attributes, not classes

Variants

Two visual styles — outline (default, border visible at rest) and fill (tinted background, no border).

html
<!-- default: bare ori-textarea is already outline + primary + md -->
<div class="ori-textarea ori-textarea_outline">
    <label class="ori-textarea__label" for="t1">Outline</label>
    <textarea id="t1" rows="3" placeholder="Type here..." class="ori-textarea__field"></textarea>
</div>

<!-- swap the variant: ori-textarea_outline → ori-textarea_fill -->
<div class="ori-textarea ori-textarea_fill">
    <label class="ori-textarea__label" for="t1b">Fill</label>
    <textarea id="t1b" rows="3" placeholder="Type here..." class="ori-textarea__field"></textarea>
</div>
vue
<OriTextarea v-model="value" label="Outline" placeholder="Type here..." variant="outline" />
<OriTextarea v-model="value" label="Fill" placeholder="Type here..." variant="fill" />

Colors

The ori-color_* class controls the focus ring (and the danger color inherits for error text).

html
<div class="ori-textarea ori-color_danger">
    <label class="ori-textarea__label" for="t2">danger</label>
    <textarea id="t2" rows="3" class="ori-textarea__field"></textarea>
</div>
vue
<OriTextarea v-model="value" label="primary" color="primary" />
<OriTextarea v-model="value" label="danger" color="danger" />

Sizes

xsxxl. The size drives both the field min-height (via ori-textarea_* size sugar on the wrapper) and the text scale (ori-font-size_*).

html
<!-- ori-textarea_xl sets min-height on the wrapper; ori-font-size_xl scales the text -->
<div class="ori-textarea ori-textarea_xl ori-font-size_xl">
    <label class="ori-textarea__label" for="t3">xl</label>
    <textarea id="t3" rows="3" class="ori-textarea__field"></textarea>
</div>
vue
<OriTextarea v-model="value" label="sm" size="sm" />
<OriTextarea v-model="value" label="xl" size="xl" />

Radius

From zero (square) through the default md to rounded (heavily rounded corners).

html
<textarea class="ori-textarea__field ori-size-radius_zero"></textarea>
vue
<OriTextarea v-model="value" label="zero" radius="zero" />
<OriTextarea v-model="value" label="rounded" radius="rounded" />

Rows

rows controls the initial visible height (and acts as the min-height). The field still grows as the user types and remains manually resizable.

html
<textarea rows="2" class="ori-textarea__field …"></textarea>
<textarea rows="5" class="ori-textarea__field …"></textarea>
vue
<OriTextarea v-model="value" label="2 rows" :rows="2" />
<OriTextarea v-model="value" label="5 rows" :rows="5" />

Label, hint & required

label renders a <label> linked to the field. hint renders a helper line below. required adds the real HTML attribute and a visual asterisk (hidden from assistive tech).

Markdown is supported.

html
<div class="ori-textarea">
    <label class="ori-textarea__label" for="bio">
        Bio<span class="ori-textarea__required" aria-hidden="true">*</span>
    </label>
    <textarea
        id="bio"
        rows="3"
        required
        placeholder="Tell us about yourself..."
        aria-describedby="bio-hint"
        class="ori-textarea__field"
    ></textarea>
    <p id="bio-hint" class="ori-textarea__hint">Markdown is supported.</p>
</div>
vue
<OriTextarea v-model="bio" label="Bio" placeholder="Tell us about yourself..." hint="Markdown is supported." required />

States

disabled uses the real attribute (also disables the resize handle). error renders a role="alert" message and sets aria-invalid. invalid flips aria-invalid without a message (for external validation).

html
<!-- disabled -->
<textarea class="ori-textarea__field …" disabled></textarea>

<!-- invalid without message -->
<textarea class="ori-textarea__field …" aria-invalid="true"></textarea>

<!-- error: message gets role=alert and is referenced by aria-describedby -->
<textarea class="ori-textarea__field …" aria-invalid="true" aria-describedby="msg-error"></textarea>
<p id="msg-error" class="ori-textarea__error" role="alert">Message must be at least 20 characters.</p>
vue
<OriTextarea v-model="value" label="Disabled" disabled />
<OriTextarea v-model="value" label="Invalid (no message)" invalid />
<OriTextarea v-model="value" label="With error" error="Message must be at least 20 characters." />

Fluid

fluid stretches the wrapper to the full width of its container.

html
<div class="ori-textarea ori-textarea_fluid …">…</div>
vue
<OriTextarea v-model="value" label="Full width" fluid />

Common patterns

A real-world support form — label, required, hint, error, and fluid compose freely.

Attachments can be added on the next step.

html
<form style="display: flex; flex-direction: column; gap: 1rem; max-width: 32rem">
    <div class="ori-textarea ori-textarea_fluid">
        <label class="ori-textarea__label" for="sub">
            Subject<span class="ori-textarea__required" aria-hidden="true">*</span>
        </label>
        <textarea
            id="sub"
            rows="3"
            required
            placeholder="Briefly describe the issue"
            class="ori-textarea__field"
        ></textarea>
    </div>
    <div class="ori-textarea ori-textarea_fluid">
        <label class="ori-textarea__label" for="msg">
            Message<span class="ori-textarea__required" aria-hidden="true">*</span>
        </label>
        <textarea
            id="msg"
            rows="6"
            required
            placeholder="Provide as much detail as you can..."
            aria-describedby="msg-hint"
            class="ori-textarea__field"
        ></textarea>
        <p id="msg-hint" class="ori-textarea__hint">Attachments can be added on the next step.</p>
    </div>
</form>
vue
<form style="display: flex; flex-direction: column; gap: 1rem; max-width: 32rem">
    <OriTextarea v-model="subject" label="Subject" placeholder="Briefly describe the issue" required fluid />
    <OriTextarea
        v-model="message"
        label="Message"
        placeholder="Provide as much detail as you can..."
        hint="Attachments can be added on the next step."
        :rows="6"
        required
        fluid
    />
</form>

Accessibility

The accessibility contract holds across every layer — the standalone classes and the Vue component render the same attributes and ARIA wiring.

  • label is associated with the field via for/id; the id is auto-generated (useId) when you don't pass one, so the association holds even without an explicit id prop.
  • hint and error are wired through aria-describedby, referencing only the element that is actually rendered (error supersedes hint). Pass extra ids with describedby to reference additional descriptions (e.g. a shared form note).
  • error sets aria-invalid="true" and announces via role="alert"; invalid flips aria-invalid on its own for external validation flows.
  • Uses the real disabled attribute and native required; the focus ring is always visible and switches to the danger color when invalid.
  • All native attributes (name, autocomplete, maxlength, wrap, spellcheck, …) fall through to the underlying <textarea> — OriTextarea stays out of the way.
KeyAction
TabMoves focus to / away from the field.

Framework API

The props, events, and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above. (Svelte bindings are planned.)

Props

PropTypeDefaultDescription
colorThemeColor'primary'Accent color for focus ring: primary · secondary · success · warn · danger · info · surface · background.
describedbystringExtra element id(s) appended to aria-describedby (e.g. a shared form note).
disabledbooleanfalseReal disabled attribute; blocks interaction, dims the field, and disables the resize handle.
errorstringError message rendered below the field (role="alert"); also sets aria-invalid="true".
fluidbooleanfalseFull-width — stretches wrapper to 100 % of its container.
hintstringHelper text below the field; hidden while error is shown.
idstringExplicit id for the <textarea>; auto-generated via useId when omitted.
invalidbooleanfalseSets aria-invalid="true" without rendering an error message (for external validation).
labelstringVisible <label> text, wired to the field via for/id.
placeholderstringNative placeholder text.
radiusRadiusSize'md'Corner radius of the field (zero · xs · sm · md · lg · xl · rounded).
requiredbooleanfalseNative required attribute; also renders a visual asterisk (aria-hidden).
rowsnumber3Visible rows of text — sets the field min-height; the field still grows and is resizable.
sizeActionSize'md'Field min-height + text scale (xs · sm · md · lg · xl · xxl).
variant'fill' | 'outline''outline'Visual style: outline (border) or fill (tinted background).

Events & attributes

v-model binds to a string via defineModel<string>() — it reads the modelValue prop and emits update:modelValue on input, so you use it like any controlled field:

vue
<OriTextarea v-model="myValue" label="Message" />

OriTextarea sets inheritAttrs: false and spreads $attrs onto the underlying <textarea>, so native attributes and listeners pass through directly to the field element — not the wrapper <div>:

  • Attributes: name, autocomplete, maxlength, minlength, wrap, spellcheck, readonly, …
  • Listeners: @input, @change, @focus, @blur, @keydown, …

Slots

OriTextarea exposes no named slots.

SlotDescription
(none)No slots are defined.