GrTextarea

  • core
  • forms
  • API · 21 props
  • ⌨ keys

A multi-line field for long text and comments.

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

Overview

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 — autosize 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 themselves — resize 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

Variants every tone and view, rendered by the real component

state

  • default
  • success
  • warning
  • danger

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • readonly
  • invalid

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • A reply that is billed by the part

    An SMS goes out in parts of 153 characters, and each part is billed, so the count slot speaks in parts and in what is left of the current one — no show-count needed, the slot is the request. The counter stays tied to the field through aria-describedby; maxlength holds the reply to three parts, and clearable empties the draft in one move, reported with clear.

    Reply to Ana Ribeiro by SMSReplies go to +351 912 004 118
    1 part · 47 left in this part
  • Release notes for a ten-line panel

    autosize grows the field with the notes so the draft is in sight, and resize="none" drops the handle that would fight it. show-line-count with max-lines counts logical lines — the in-app panel cuts at ten — and never cuts the text for the author. Past ten the page sets state="warning", which adds an icon and a hidden label: colour alone is not a message.

    Shown in the app after the update, ten lines at most

    5 / 10
  • A delivery address and a note

    autocomplete="street-address" lets the browser fill the whole address at once, and a postcode it recognises turns the border success once the field is left. After pick-up the address is readonly — selectable and still sent — while the note, a smaller size with a show-count limit, is disabled and stays out of the form.

    Only the courier sees it

    48 / 140

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseSwitch the field off: the value is not edited and does not go into the form. It adds up with `GrFormField`.
readonlyboolean | undefinedfalseRead only: the value is visible and goes into the form but is not edited.
invalidboolean | undefinedfalseA validation error: a danger border and `aria-invalid`. It adds up with the error of `GrFormField`.
requiredboolean | undefinedfalseA mandatory field (`aria-required`). It adds up with the `required` of `GrFormField`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe type size and the paddings by the controls scale. `md` by default; taken from `GrConfigProvider`.
placeholderstring | undefinedundefinedA hint in an empty field. It does not replace the label: the name is given by `GrFormField` or `ariaLabel`.
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 | undefinedundefinedThe native `name` — the name of the field when the form is submitted.
idstring | undefinedundefinedThe `id` of the field. Not set — taken from `GrFormField`, so that the label finds the field.
rowsnumber | undefined4The height in rows; with `autosize` — the initial and minimum one. `4` by default.
state"default" | "success" | "warning" | "danger" | undefined"default"The highlight of the border at the developer's choice. `success` and `warning` add an icon and a hidden label; `invalid` is stronger. `default` by default.
autocompletestring | undefinedundefinedThe native `autocomplete`: what the browser should fill the field with — `street-address`, for instance.

Slots

SlotTypeDescription
count
{ length: number; maxlength?: number | undefined; remaining?{ length: number; maxlength?: number | undefined; remaining?: number | undefined; }
A wording of your own for the character counter instead of `12 / 60` — "48 left", "almost the limit" and anything else. There are two built-in counters, but both are fixed in form, and a second ready-made wording does not replace the first.

Events

EventSignatureDescription
update:modelValue(event: "update:modelValue", value: string): void—
change(event: "change", value: string): void—
clear(event: "clear"): void—
focus(event: "focus", event: FocusEvent): void—
blur(event: "blur", event: FocusEvent): void—

Methods

MethodsSignatureDescription
focus() => voidFocus into the field from code.
blur() => voidRemove focus from the field.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrTextareaProps, GrTextareaEmits, GrTextareaConfigurableProps } from '@feugene/granularity/components/GrTextarea'

Filedist/types/components/GrTextarea/GrTextarea.vue.d.ts

GrTextareaPropsinterface
interface GrTextareaProps {
    modelValue: string;
    placeholder?: string;
    autocomplete?: string;
    disabled?: boolean;
    readonly?: boolean;
    invalid?: boolean;
    required?: boolean;
    ariaLabel?: string;
    state?: GrTextareaState;
    name?: string;
    id?: string;
    rows?: number;
    size?: GrTextareaSize;
    maxlength?: number;
    showCount?: boolean;
    showLineCount?: boolean;
    maxLines?: number;
    autosize?: boolean;
    clearable?: boolean;
    clearLabel?: string;
    resize?: GrTextareaResize;
}
GrTextareaEmitsinterface
interface GrTextareaEmits {
    (e: 'update:modelValue', value: string): void;
    (e: 'change', value: string): void;
    (e: 'clear'): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
}

Filedist/types/components/GrTextarea/config.d.ts

grTextareaConfigconst
const grTextareaConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrTextarea">;

Filedist/types/components/GrTextarea/defaults.d.ts

GrTextareaConfigurablePropsinterface
interface GrTextareaConfigurableProps {
    size: GrComponentSize;
    clearable: boolean;
}

Filedist/types/components/GrTextarea/grTextareaStyles.d.ts

GrTextareaStatetype
type GrTextareaState = GrControlState;
GrTextareaSizetype
type GrTextareaSize = GrComponentSize;

Filedist/types/components/GrTextarea/safelist.d.ts

grTextareaSafelistconst
const grTextareaSafelist: string[];

Filedist/types/components/GrTextarea/index.d.ts

GrTextareaInstancetype
type GrTextareaInstance = ComponentExposed<typeof GrTextareaComponent>;

Accessibility

APG pattern

—

Usage from the library prose

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. The field keeps room for the cross on the right while it can appear — the first line does not slide under the ”×”.

The wrapper around the textarea is always there, while the button, the state mark and the counters (showCount, showLineCount) are its conditional siblings. The wrapper used to appear together with them, and the <textarea> was re-created: as soon as state became success on the fly, focus went to <body>, and the following keystrokes were lost. class and style land on the wrapper, the other attributes on the <textarea> itself (as with GrInput, see form-controls.md). The field inside is a block: its height is the same with the siblings and without them.

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.

state="success" and "warning" get an icon and a hidden label in aria-describedby by the same device: the colour of the border as the only carrier of meaning is WCAG 1.4.1, and these two states have no ARIA equivalent of aria-invalid. danger gets no icon — its meaning is already carried by aria-invalid and the error text from GrFormField — and the mark goes out as soon as the field is declared invalid.

The mark appears in the same wrapper as the clear button: the field is not re-created in the process, and focus and the caret stay in place.

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.

A wording of your own

12 / 60 is not the only way to speak about the length, and the line counter does not replace a second wording: it is simply a second ready counter, just as fixed in form. The #count slot hands the text over to the consumer:

vue
<GrTextarea v-model="text" :maxlength="280">
  <template #count="{ remaining }">
    {{ remaining < 0 ? `${-remaining} over` : `${remaining} left` }}
  </template>
</GrTextarea>

The slot receives length, maxlength and remaining; without maxlength the last two arrive as undefined — a limit is not invented. remaining is not clamped to zero: maxlength holds input from the keyboard, but a value that came into v-model from code steps over the limit, and “-3” there is more honest than zero.

A separate show-count is not needed for that — a slot that has been given is in itself a request to show the counter. The place in the row, the link to the field through aria-describedby and the styling stay the same: the slot changes the text rather than the mechanism.

Automatic height fitting
vue
<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.