GrTextarea
A multi-line field for long text and comments.
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the text is longer than a line — a comment, a description, a letter: line breaks are part of the value;
- the height adjusts —
autosizewithmaxLinesgrows with the content and does not eat up the screen; - the length is limited — a counter of characters and lines beside the field;
- the user changes the size themselves —
resizeinstead of a fixed height.
When to take something else
| Need | Take |
|---|---|
| The value is a single line | GrInput |
| Text formatting is needed | there is no rich-text editor in the ecosystem yet |
| A number is entered | GrNumberInput |
| A long text is asked for with a window | GrPromptDialog with multiline |
The events
| Event | When |
|---|---|
update:modelValue | on every keystroke |
change | the value is committed: the native change (on the loss of focus) or the clear button |
clear | the value was erased with the clear button (clearable) |
focus, blur | with a FocusEvent object |
The set matches GrInput, so that wrappers over the controls are written the same
way. The native events are re-emitted by the component: a declared emit leaves $attrs, and without
that the consumer’s @change would stop working.
clearable repeats the anatomy of GrInput: a cross in the upper right corner when the value is
non-empty, hidden with disabled/readonly, configured through GrConfigProvider. A wrapper
around the textarea appears only for the button — without clearable and the counters (showCount,
showLineCount) the field remains the root element, and the contract of fallthrough attributes does
not change.
Parity with `GrInput`
size, readonly, maxlength + showCount work the same way as in the input field: a textarea in
one form next to a GrInput must differ neither in type size nor in the set of possibilities.
The counter is linked to the field through aria-describedby — otherwise “12 / 60” is seen with the
eyes but not heard, while the limit on the length is its whole point.
The line counter
showLineCount prints a second label in the same row: the lines on the left, the characters on the
right. The lines are counted logically — by line breaks — so with autosize the number does not
depend on the width of the field: a visual wrap does not count as a line.
The label is localised and inflected (gr.textarea.lines), and maxLines changes it to the 3 / 10
format. It deliberately has no limit on the input: the component has no right to truncate typed text
on the user’s behalf — the counter shows the excess (12 / 10), and the decision stays with the
form.
Both counters are switched on independently and both are linked to the field through
aria-describedby.
Automatic height fitting
<GrTextarea v-model="text" autosize :rows="2" />
autosize switches on the v-autosize directive, which was already in the package and simply had
not been connected to the component. rows sets the starting height.
resize (vertical by default, none, both) governs manual stretching: together with autosize
it is usually switched off.
The type of the props
GrTextareaProps is a declared interface rather than a typeof props. The previous export gave
away the type of the resolved props: after withDefaults all of the fields with defaults became
required and readonly, and an attempt to write const p: GrTextareaProps = { modelValue: '' }
failed out of the blue.
The disabled state
It is dimmed with the --gr-muted background and --gr-muted-fg text rather than with opacity:
transparency dilutes tokens tuned to AA.
Playground 20
Loading…
<GrTextarea />Install
npm i @feugene/granularityImport
import { GrTextarea } from '@feugene/granularity/components/GrTextarea'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Read only: the value is visible and goes into the form but is not edited. |
invalid | boolean | undefined | false | — |
required | boolean | undefined | false | A mandatory field (`aria-required`). It adds up with the `required` of `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
placeholder | string | undefined | undefined | — |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`. |
clearable | boolean | undefined | undefined | A button that clears the value. It is configured through `GrConfigProvider`. |
clearLabel | string | undefined | undefined | The a11y label of the clear button. |
name | string | undefined | undefined | — |
id | string | undefined | undefined | — |
rows | number | undefined | 4 | — |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | — |
autocomplete | string | undefined | undefined | — |
maxlength | number | undefined | undefined | A limit on the length plus the basis for the counter of characters. |
showCount | boolean | undefined | false | Show the counter of characters (`len` or `len/maxlength`). |
autosize | boolean | undefined | false | Fit the height to the content (the `v-autosize` directive). |
resize | GrTextareaResize | undefined | "vertical" | Manual resizing by the user. |
modelValuerequired | string | — | — |
showLineCount | boolean | undefined | — | Show the counter of lines. The **logical** lines are counted (the line breaks) rather than the visual wraps: with `autosize` the number does not change with the width of the field. |
maxLines | number | undefined | — | A reference number of lines for the counter (`3 / 10`). It does not limit the input: the component has no right to cut the text typed on the user’s behalf. |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | [value: string] | — |
change | [value: string] | — |
clear | [] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Description |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Examples 7
Line Count
Счётчик строк слева, символов — справа
maxLines задаёт ориентир и не режет набранное
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const changelog = ref('fix: чипы автокомплита видно на светлой теме\nfeat: счётчик строк\n')
const script = ref('INT. OFFICE — DAY\n\nОна открывает ноутбук.\n')
</script>
<template>
<div class="grid gap-4">
<GrFormField label="Запись в CHANGELOG" hint="Счётчик строк слева, символов — справа">
<GrTextarea
v-model="changelog"
show-line-count
show-count
:maxlength="240"
:rows="4"
/>
</GrFormField>
<GrFormField label="Сцена" hint="maxLines задаёт ориентир и не режет набранное">
<GrTextarea
v-model="script"
show-line-count
:max-lines="8"
autosize
:rows="3"
/>
</GrFormField>
</div>
</template>Autosize
Высота подстраивается под содержимое
Счётчик связан с полем через aria-describedby
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const comment = ref('Поле растёт под текст: директива `v-autosize` была в пакете и просто не была подключена.')
const summary = ref('')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<GrFormField label="Комментарий" hint="Высота подстраивается под содержимое">
<GrTextarea
v-model="comment"
autosize
resize="none"
:rows="2"
placeholder="Что изменилось в релизе"
/>
</GrFormField>
<GrFormField label="Краткое описание" hint="Счётчик связан с полем через aria-describedby">
<GrTextarea
v-model="summary"
:maxlength="120"
show-count
:rows="3"
size="sm"
placeholder="До 120 символов"
/>
</GrFormField>
</div>
</template>Clearable
Крестик появляется, когда есть что стирать
Кнопка очистки достижима `Tab`, срабатывает `Enter` и `Space`
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const note = ref('Клиент просил перезвонить после 18:00.\nНапомнить про счёт за март.')
const draft = ref('')
</script>
<template>
<div class="grid gap-4">
<GrFormField label="Заметка по клиенту" hint="Крестик появляется, когда есть что стирать">
<GrTextarea v-model="note" clearable :rows="3" />
</GrFormField>
<GrFormField label="Черновик письма" hint="Кнопка очистки достижима `Tab`, срабатывает `Enter` и `Space`">
<GrTextarea
v-model="draft"
clearable
show-count
:maxlength="200"
:rows="3"
placeholder="Наберите текст — появится счётчик и крестик"
/>
</GrFormField>
</div>
</template>Default and expanded rows
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrTextarea } from '@feugene/granularity'
const shortNote = ref('Release notes highlight the latest API additions.')
const longNote = ref('This textarea starts taller and fits editorial copy, migration notes or incident postmortems.')
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<GrFormField label="Default rows">
<GrTextarea v-model="shortNote" placeholder="Write a short note" />
</GrFormField>
<GrFormField label="Expanded rows">
<GrTextarea v-model="longNote" :rows="8" placeholder="Long-form content" />
</GrFormField>
</div>
</template>Success and validation states
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrSwitch, GrTextarea } from '@feugene/granularity'
const draft = ref('Ship the new showcase after validating all public entities.')
const reviewNotes = ref('')
const invalid = ref(false)
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_220px]">
<div class="grid gap-3">
<GrFormField label="Success state">
<GrTextarea v-model="draft" state="success" />
</GrFormField>
<GrFormField label="Validation state" :error="invalid ? 'Review notes are required before publishing' : undefined">
<GrTextarea
v-model="reviewNotes"
placeholder="Add review notes"
:invalid="invalid"
:state="invalid ? 'danger' : 'default'"
/>
</GrFormField>
</div>
<div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
<div class="text-sm font-semibold text-[var(--gr-fg)]">
Validation toggle
</div>
<GrSwitch v-model="invalid" size="sm">
Mark review notes as required
</GrSwitch>
</div>
</div>
</template>Disabled review and audit mode
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrSwitch, GrTextarea } from '@feugene/granularity'
const disabled = ref(false)
const summary = ref('Subscribers will receive a digest every Monday at 09:00.')
</script>
<template>
<div class="grid gap-4">
<div class="flex items-center gap-3">
<GrSwitch v-model="disabled" size="sm">
Disable textarea
</GrSwitch>
</div>
<GrFormField label="Operational notes">
<GrTextarea
v-model="summary"
:disabled="disabled"
:rows="6"
placeholder="Editable summary"
/>
</GrFormField>
</div>
</template>Sizes
<script setup lang="ts">
import { ref } from 'vue'
import { GrFormField, GrInput, GrTextarea } from '@feugene/granularity'
const sizes = ['xs', 'sm', 'md', 'lg'] as const
const title = ref('Weekly digest')
const note = ref('Subscribers receive this summary every Monday at 09:00.')
</script>
<template>
<div class="grid gap-4">
<div v-for="size in sizes" :key="size" class="grid gap-2">
<div class="text-xs font-semibold text-[var(--gr-muted-fg)]">
size="{{ size }}"
</div>
<GrFormField label="Title">
<GrInput v-model="title" :size="size" />
</GrFormField>
<GrFormField label="Note">
<GrTextarea v-model="note" :size="size" :rows="2" />
</GrFormField>
</div>
</div>
</template>Accessibility
- APG pattern
—