Keyboard shortcut: Ctrl + K
Get started

Textarea

A multi-line text field, with the same chrome as VInput: label above, hint below, icons inside, a counter and a clear button. It can grow as the text is typed.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const message = ref('')
</script>

<template>
  <VTextarea v-model="message" label="Message" placeholder="What would you like to tell us?" />
</template>

Examples

Label and hint

label is a real <label> tied to the field, so clicking the words puts the cursor in the box. hint goes under the field and is tied to it through aria-describedby.

vue

Two or three sentences. It appears on your public profile.

<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const bio = ref('')
</script>

<template>
  <VTextarea
    v-model="bio"
    label="Short bio"
    hint="Two or three sentences. It appears on your public profile."
    placeholder="What do you work on?"
  />
</template>

Sizes

size sets the padding, the type scale and the icons, never the height, which comes from rows. compact takes 4px off the padding at any of the three.

vue
<script setup lang="ts">
import { VTextarea } from 'vectis-ui'

const sizes = ['sm', 'md', 'lg'] as const
</script>

<template>
  <div class="grid">
    <template v-for="size in sizes" :key="size">
      <VTextarea :size="size" :label="size" :rows="2" />
      <VTextarea :size="size" :label="`${size} compact`" :rows="2" compact />
    </template>
  </div>
</template>

<style scoped>
/* Two columns wherever there is room for them, so each size is read against its own
   compact form rather than against the size above it. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--vectis-space-4);
}
</style>

Icons

iconStart and iconEnd place a decorative icon at either end, on the first line rather than in the middle of the box. The #end slot replaces the end icon, where #start is rendered after the start icon rather than in its place.

vue
<script setup lang="ts">
import { VTextarea } from 'vectis-ui'
import { description, info } from 'vectis-ui/icons'
</script>

<template>
  <VTextarea :icon-start="description" label="Start icon" :rows="2" />
  <VTextarea :icon-end="info" label="End icon" :rows="2" />
  <VTextarea :icon-start="description" :icon-end="info" label="Both" :rows="2" />
</template>

Clickable icons

A @click:icon-start or @click:icon-end listener turns that icon into a real button, which then needs its label. Each button is its own tab stop and stays outside the textarea.

vue

Nothing attached yet.

<script setup lang="ts">
import { computed, ref } from 'vue'
import { VTextarea } from 'vectis-ui'
import { attach_file as attachFile, code } from 'vectis-ui/icons'

const message = ref('')
const attachments = ref(0)

const hint = computed(() => {
  if (attachments.value === 0) return 'Nothing attached yet.'
  return attachments.value === 1 ? '1 file attached.' : `${attachments.value} files attached.`
})

function insertCodeBlock() {
  message.value += '\n```\n\n```'
}
</script>

<template>
  <VTextarea
    v-model="message"
    label="Message"
    :hint="hint"
    :rows="4"
    :icon-start="code"
    icon-start-label="Insert a code block"
    :icon-end="attachFile"
    icon-end-label="Attach a file"
    @click:icon-start="insertCodeBlock"
    @click:icon-end="attachments += 1"
  />
</template>

Clearable

clearable adds a cross that empties the field, shown while there is something to clear and the field can be edited. Pressing it hands the focus straight back to the textarea, and clear fires after the fact.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const draft = ref('A first attempt, worth throwing away in one go.')
</script>

<template>
  <VTextarea v-model="draft" label="Draft" clearable :rows="3" />
</template>

Counters

counter goes under the field, beside the hint. Against maxlength the browser refuses everything past the limit, where softLimit lets the reader type on: the counter goes red and the field reports itself invalid through the native validity. It counts characters, as on VInput; on VFileInput the same prop counts files and their size.

vue

The browser refuses the eighty-first character.

22/80

Type past eighty and the field goes into error instead.

0/80

No limit, so the counter only counts.

0
<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const summary = ref('Vue developer in Lyon.')
const review = ref('')
const note = ref('')
</script>

<template>
  <VTextarea
    v-model="summary"
    label="Summary"
    hint="The browser refuses the eighty-first character."
    :maxlength="80"
    counter
    :rows="2"
  />

  <VTextarea
    v-model="review"
    label="Review"
    hint="Type past eighty and the field goes into error instead."
    :maxlength="80"
    soft-limit
    counter
    :rows="3"
  />

  <VTextarea
    v-model="note"
    label="Note"
    hint="No limit, so the counter only counts."
    counter
    :rows="2"
  />
</template>

Auto grow

rows gives the field its starting height, and by default its height full stop. autoGrow lets the box get taller as the text is typed, in pure CSS.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const text =
  'Both fields start two rows tall.\nAdd a line to each of them.\nThe first one scrolls, the second one gets taller.'

const fixed = ref(text)
const growing = ref(text)
</script>

<template>
  <VTextarea v-model="fixed" label="Fixed height" :rows="2" />
  <VTextarea v-model="growing" label="Grows with the text" :rows="2" auto-grow />
</template>

States

invalid is for a rule the browser cannot check by itself. disabled greys the field out through the colour tokens. readonly can still be focused and copied from, and hides the clear cross. loading puts a spinner where the end icon goes, the field staying usable.

vue

Say a little more than that.

Loading…
<script setup lang="ts">
import { ref } from 'vue'
import { VTextarea } from 'vectis-ui'

const invalid = ref('Too short')
const disabled = ref('Waiting on the form to be submitted')
const readonly = ref('Signed off on 12 March, and no longer open to changes')
const loading = ref('Writing a summary')
</script>

<template>
  <VTextarea
    v-model="invalid"
    label="Invalid"
    hint="Say a little more than that."
    invalid
    :rows="2"
  />
  <VTextarea v-model="disabled" label="Disabled" disabled :rows="2" />
  <VTextarea v-model="readonly" label="Read only" readonly :rows="2" />
  <VTextarea v-model="loading" label="Loading" loading :rows="2" />
</template>

API

Props

PropTypeDefault
sizeTextareaSize'sm' | 'md' | 'lg''md'
The size of the field, which sets its padding, its type scale and its icons.
compactbooleanfalse
Takes 4px off the field by tightening its padding, leaving the number of lines, the type and the icons alone.
rowsnumber5
How many lines of text the field shows, the native rows attribute, which is what gives the field its height. Anything under 1 is raised to 1, and at 1 the field is exactly as tall as a VInput of the same size.
autoGrowbooleanfalse
Lets the field grow as the text is typed, instead of scrolling inside the height rows gives it, which stays its starting height. It is pure CSS: where the browser does not support it, the field behaves like an ordinary textarea.
invalidbooleanfalse
Marks the field as invalid whatever the browser thinks, the route for a rule only the server can check.
disabledbooleanfalse
Makes the field unusable, greyed out through the colour tokens.
readonlybooleanfalse
Shows the text without allowing it to be changed. The field can still be focused and copied from, and the clear button is hidden.
labelstringnone
The label above the field, tied to it so that clicking it focuses the field.
hintstringnone
A line of help under the field, tied to the textarea for assistive technology so that it is read out along with the label.
iconStartIconSourcenone
An icon inside the field, at the start. It is decorative until a @click:icon-start listener is attached, at which point it becomes a real button and needs iconStartLabel.
iconEndIconSourcenone
The same at the end of the field. The #end slot replaces it, and the loading spinner takes its place while it turns.
iconStartLabelstringnone
What the start icon does, in words, once it is clickable.
iconEndLabelstringnone
What the end icon does, in words, once it is clickable.
loadingbooleanfalse
Shows a spinner at the end of the field, in place of the end icon or slot.
loadingTextstringnone
What screen readers announce while the spinner turns. It falls back to the design system dictionary.
clearablebooleanfalse
Offers a cross that empties the field. It appears when there is something to clear and the field can be edited.
clearVisiblebooleannone
Decides whether the cross is shown, instead of letting the field work it out from its own content, a read-only field included. It is the same escape hatch VInput offers, for components built on top of this one that hold what there is to clear somewhere other than the text.
clearLabelstringnone
What the clear button does, in words. It falls back to the design system dictionary.
maxlengthnumbernone
The maximum number of characters. By default this is the browser's own limit, which simply refuses anything beyond it.
softLimitbooleanfalse
Turns that limit into a soft one: the reader may type past it, and the field goes into error instead of silently refusing the keystrokes. It is reported through the native validity, so a form cannot be submitted over the limit.
counterbooleanfalse
Shows how much has been typed, under the field: 12/80 against a limit, or just 12 without one.
v-modelstring''
The text in the field, empty to begin with.

Events

EventType
click:icon-start[event: MouseEvent]
The start icon was pressed. Attaching this listener is what turns it into a button.
click:icon-end[event: MouseEvent]
The end icon was pressed. Attaching this listener is what turns it into a button.
clear[]
The clear button was pressed. The value has already been emptied.

Slots

SlotType
start{}
Content at the start of the field, rendered after iconStart rather than in its place.
end{}
Content at the end of the field, which replaces iconEnd. It is hidden while the field is loading, the spinner taking that place.
value-end{}
Controls of your own inside the field, placed before the ones the field owns: the clear cross and the icon that opens the panel. Those two are the component own affordance, which is why there is no end slot here.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender