GrTextarea

Package: @feugene/granularitycoreGroup: forms

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 adjustsautosize with maxLines grows 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 themselvesresize instead of a fixed height.

When to take something else

NeedTake
The value is a single lineGrInput
Text formatting is neededthere is no rich-text editor in the ecosystem yet
A number is enteredGrNumberInput
A long text is asked for with a windowGrPromptDialog with multiline

The events

EventWhen
update:modelValueon every keystroke
changethe value is committed: the native change (on the loss of focus) or the clear button
clearthe value was erased with the clear button (clearable)
focus, blurwith 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…

Code
<GrTextarea />

Install

npm i @feugene/granularity

Import

import { GrTextarea } from '@feugene/granularity/components/GrTextarea'

API

Props

PropTypedefaultDescription
disabledboolean | undefinedfalse
readonlyboolean | undefinedfalseRead only: the value is visible and goes into the form but is not edited.
invalidboolean | undefinedfalse
requiredboolean | undefinedfalseA mandatory field (`aria-required`). It adds up with the `required` of `GrFormField`.
size"xs" | "sm" | "md" | "lg" | undefinedundefined
placeholderstring | undefinedundefined
ariaLabelstring | undefinedundefinedThe accessible name outside `GrFormField`.
clearableboolean | undefinedundefinedA button that clears the value. It is configured through `GrConfigProvider`.
clearLabelstring | undefinedundefinedThe a11y label of the clear button.
namestring | undefinedundefined
idstring | undefinedundefined
rowsnumber | undefined4
state"default" | "success" | "warning" | "danger" | undefined"default"
autocompletestring | undefinedundefined
maxlengthnumber | undefinedundefinedA limit on the length plus the basis for the counter of characters.
showCountboolean | undefinedfalseShow the counter of characters (`len` or `len/maxlength`).
autosizeboolean | undefinedfalseFit the height to the content (the `v-autosize` directive).
resizeGrTextareaResize | undefined"vertical"Manual resizing by the user.
modelValuerequiredstring
showLineCountboolean | undefinedShow 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.
maxLinesnumber | undefinedA 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

EventTypeDescription
update:modelValue[value: string]
change[value: string]
clear[]
focus[event: FocusEvent]
blur[event: FocusEvent]

Methods / Expose

Methods / ExposeTypeDescription
focus() => void
blur() => void

Examples 7

Line Count

Счётчик строк слева, символов — справа

3 lines
65 / 240

maxLines задаёт ориентир и не режет набранное

4 / 8

Line Count
<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

0 / 120

Autosize
<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`

0 / 200

Clearable
<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

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

Validation toggle

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

Disabled
<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

size="xs"
size="sm"
size="md"
size="lg"

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

Full keyboard contract of the package

Component documentationAll components