GrInput

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

A single-line field for text, search and short values.

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

Overview

When to take it

  • a string is entered — a name, an address, a search query, a password: the basic control of a form;
  • the field has addons — a unit of measurement, the prefix of a protocol, a search icon through the slots;
  • the length is limited — maxlength with a character counter instead of silent truncation;
  • background work is going on in the field — a busy indicator that does not block typing.

When to take something else

NeedTake
Text across several linesGrTextarea
A number with steps is enteredGrNumberInput
There are several values, as chipsGrInputTag
A choice among ready optionsGrSelect
A search with suggestionsGrAutocomplete
A date or a timeGrDatePicker

Variants every tone and view, rendered by the real component

Variants · GrInput
state \ shapeboxpill
default
success
warning
danger

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • disabled
  • loading
  • 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 product editor with addons

    Addons hold what is not the value — the shop address, the currency, the unit — and describe-addons lets a screen reader hear them where they carry meaning. suffix-fixed with one suffix-min-width keeps EUR and kg the same width, so the numbers form a column, and text-align="right" lines up their digits.

    Linen shirt, sand
    shop.example.com/p/
    EUR
    EUR
    kg
  • A username checked as you type

    loading shows the availability check inside the field and sets aria-busy; state colours the answer and adds its icon. maxlength with show-count keeps the limit in sight, and password-toggle adds the eye with a proper accessible name.

    Create your account

    Letters, digits and hyphens.

    4 / 20

    12 characters or more

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseSwitch the field off: the clear button and the password reveal disappear. Combined with `GrFormField`.
type"number" | "search" | "text" | "email" | "password" | "tel" | "url" | undefined"text"The native `type`: it changes the keyboard and the browser's format check. `text` by default.
modelValuev-modelstring | undefined""The value of the field. Optional: without a `v-model` the field is drawn empty. The default is an empty string rather than `undefined`: the length of the value is read directly (`showClear`), and an `undefined` would crash the render.
readonlyboolean | undefinedfalseRead-only: the value is visible and selectable, but is not edited.
invalidboolean | undefinedfalseA validation error: a danger border and `aria-invalid`. Combined with the error of `GrFormField`.
requiredboolean | undefinedfalseA required field (`aria-required`). It adds to the `required` of `GrFormField`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height and the type size 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 | undefinedundefinedShow the clear button when there is a value (and it is not disabled/readonly).
loadingboolean | undefinedfalseBackground work on the field (checking whether a login is taken, autosaving): a spinner in the trailing area plus `aria-busy`. Typing is not blocked — for that there are `disabled`/`readonly`.
clearLabelstring | undefinedundefinedThe i18n aria-label of the clear button.
namestring | undefinedundefinedThe native `name` — the name of the field when the form is submitted.
prefixMinWidthstring | undefinedundefinedThe minimum width of the prefix. By default — by the size step; for an `inline` addon — `0`.

Slots

SlotTypeDescription
prefixanyAn addon to the left of the field: an icon, a currency code, a label.
suffixanyAn addon to the right of the field: a unit of measurement, a hint.

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() => voidPut the focus into the field.
blur() => voidRemove the focus from the field.
select() => voidSelect the content of the field — so that a prefilled value is overwritten by typing.

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 { GrInputConfigurableProps, GrInputProps, GrInputEmits } from '@feugene/granularity/components/GrInput'

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

grInputConfigconst
const grInputConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrInput">;

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

GrInputConfigurablePropsinterface
interface GrInputConfigurableProps {
    size: GrComponentSize;
    clearable: boolean;
    shape: GrControlShape;
}

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

GrInputPropsinterface
interface GrInputProps {
    modelValue?: string;
    type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
    placeholder?: string;
    autocomplete?: string;
    inputmode?: InputHTMLAttributes['inputmode'];
    disabled?: boolean;
    readonly?: boolean;
    invalid?: boolean;
    required?: boolean;
    ariaLabel?: string;
    state?: 'default' | 'success' | 'warning' | 'danger';
    name?: string;
    id?: string;
    size?: GrInputSize;
    shape?: GrControlShape;
    clearable?: boolean;
    clearLabel?: string;
    maxlength?: number;
    showCount?: boolean;
    describeAddons?: boolean;
    loading?: boolean;
    passwordToggle?: boolean;
    passwordShowLabel?: string;
    passwordHideLabel?: string;
    textAlign?: GrInputTextAlign;
    prefixMinWidth?: string;
    prefixMaxWidth?: string;
    suffixMinWidth?: string;
    suffixMaxWidth?: string;
    prefixFixed?: boolean;
    suffixFixed?: boolean;
    addon?: 'segment' | 'inline';
}
GrInputEmitsinterface
interface GrInputEmits {
    (e: 'update:modelValue', value: string): void;
    (e: 'change', value: string): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
    (e: 'clear'): void;
}
GrInputSizetype
type GrInputSize = GrComponentSize;
GrInputTextAligntype
type GrInputTextAlign = 'left' | 'center' | 'right';

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

grInputSafelistconst
const grInputSafelist: string[];

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

GrInputInstancetype
type GrInputInstance = ComponentExposed<typeof GrInputComponent>;

Accessibility

APG pattern

—

Usage from the library prose

The trailing controls are reachable from the keyboard

The clear and show-password buttons stand in the ordinary tab order: Tab from the field leads to the cross, then to the eye. They used to carry tabindex="-1" — the widget was declared accessible (aria-label, aria-pressed), but there was no using it without a mouse.

Both buttons return the focus to the field after being pressed, so clearing does not drop the focus into nowhere: the button disappears together with the emptied value.

Addons: a segment or an ornament

#prefix and #suffix are drawn in two forms, and those are different entities rather than the styling of one.

segment (the default) is a compartment of its own, cut off by a border and aligned to the size step: a field with ”₽” and a field with “USD” stand in a column instead of dancing by width. That is what is expected of a money field and of a field with a unit.

addon="inline" is an ornament inside the border: no separator, no width of its own. The magnifier of a search bar, a currency symbol, a counter. In a segment the magnifier would read as a field with a button glued to it, so an icon inside the border could not be expressed with an addon at all — people simply gave it up.

vue
<GrInput v-model="query" addon="inline">
  <template #prefix>
    <GrIcon size="sm"><span class="i-lucide-search" /></GrIcon>
  </template>
</GrInput>

The padding of the field is computed from the measured width of the addon in both modes: the text starts right after the ornament rather than in an empty compartment. prefixFixed/suffixFixed and *MinWidth/*MaxWidth work as before — they are about the segment.

A meaningful addon is announced

Addons are hidden from the screen reader by default: most often there is a search magnifier or a glyph repeating the label of the field there, and a second reading gets in the way.

But https:// in an address field or ₽ beside a label without a currency can be the only place where it is said what to enter. For them there is describeAddons.

It does not merely lift the hiding: a <div> beside the field does not enter its accessible name, and a screen reader would stumble on the text only while walking the page — not when it is needed. That is why the addon goes into the aria-describedby of the field itself.

vue
<GrInput v-model="site" describe-addons>
  <template #prefix>
    https://
  </template>
</GrInput>

Mark a decorative icon inside the slot aria-hidden yourself: the prop lifts the hiding from the whole wrapper rather than taking its content apart.

The shape of the frame

shape="pill" gives the same rounding as GrSegmented; the default is box, the rounding of the control scale.

The shape brings the horizontal padding with it: on a pill it is no less than half the height, otherwise the text runs into the arc. The addons follow it too — the compartment is clipped along the outer contour by the shell (overflow-hidden), while its separating border stays straight.

The axis and its limits — ../sizes.md; it also says which fields of a form did not get a shape and why.

The character counter

showCount draws 12 / 60 (or just the length, if maxlength is not set) and links the counter to the field through aria-describedby — on focus a screen reader reads the remainder together with the label.

Exhausting the limit is additionally announced by a role="status" live region (gr.input.limitReached). The region stays silent until the limit is reached: reading every character aloud is a guaranteed way to make the field unusable for a screen reader.

vue
<GrInput v-model="bio" :maxlength="60" show-count clearable />
The events
EventWhen
update:modelValueon every keystroke
changethe value is committed: the native change (on blur or Enter) or the clear button
focus / blurthe focus arrived/left, the argument is a FocusEvent
clearthe value was erased with the clear button

clear exists separately from update:modelValue because by the value alone a programmatic clearing cannot be told from a manual erasure — and the reaction of a form to them is usually different.

Clearing with the button sends all three events in a row: update:modelValue, change, clear. It is as much a commitment of the value as the departure of the focus — a subscription for “the value has been set” would miss exactly it. The native equivalent behaves the same way: the cross of an <input type="search"> sends both input and change.

The imperative API
vue
<GrInput ref="field" v-model="value" />

field.value gives away focus(), blur() and select(). The last one is the companion of focus() for the “the value has been filled in, let it be overwritten” scenario.

loading

A spinner in the trailing area plus aria-busy on the field: checking whether a login is taken, autosaving, loading a reference list. Typing is not blocked in the process — for a prohibition there are disabled and readonly. The spinner reserves room on the right on a par with the buttons, so the text does not move under it.

The states and the tokens

state (success / warning / danger) colours the border and the focus ring; invalid (its own prop or an error from GrFormField) always leads to the danger look.

success and warning additionally show an icon and a hidden label that goes into the aria-describedby of the field. A border colour as the only carrier of meaning is a WCAG 1.4.1 matter: the icon adds a shape, the label adds a programmatic channel, which these two states lack (there is no ARIA equivalent of aria-invalid for them). danger gets no icon: its meaning is already carried by aria-invalid and the error text from GrFormField.

The sign goes out when the field is declared invalid: a “correct” tick beside aria-invalid="true" would contradict it.

A blocked field is dimmed with the --gr-muted background and --gr-muted-fg text rather than with transparency: opacity dilutes text tokens tuned to AA and lowers the contrast.

The classes of the sizes, the alignment and the states live in grInputStyles.ts and are declared in the safelist as a whole — together with the horizontal padding, which the same size also needs as a number (the addons set the padding with an inline style, and that overrides a class).

type

text, email, password, number, search, tel, url. The last two change the on-screen keyboard on mobile and switch on the browser’s validation of the format.