Skip to content
v2.5.0
English
Русский
Documentation

TextField

TextField is a compound component for single-line text entry.
text-field.tsx · 8 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';

<div>
  <Label htmlFor='preview-email'>Email</Label>
  <TextField>
    <TextFieldInput id='preview-email' type='email' placeholder='name@company.com' />
  </TextField>
</div>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 121
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
30 tests

Summary

TextField is a compound component for single-line text entry. The root provides shared size and error state, while TextFieldInput owns the native input element. It is the base single-line field wrapper that other specialized text-entry controls build on.

Imports

import {
  TextField,
  TextFieldInput,
  type TextFieldInputProps,
  type TextFieldProps,
  type TextFieldSize,
} from '@vega-ui/react';

Exported Types

  • TextFieldProps
  • TextFieldInputProps
  • TextFieldSize = 'sm' | 'md' | 'lg' | string

Minimal Composition

work-email-field.tsx · 24 lines
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function WorkEmailField() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Work email</Label>
      <TextField>
        <TextFieldInput

Required Parts

  • TextField: layout wrapper with size and error context
  • TextFieldInput: native input element

Optional Parts

  • prefix or suffix children such as Button, IconButton, counters, or status text

Composition Order

  1. TextField
  2. optional leading child
  3. TextFieldInput
  4. optional trailing child

Variants

  • Size: sm, md, lg
  • State: default, error, disabled input
  • Composition: plain input, prefix/suffix controls, controlled or uncontrolled value

Edge Cases

  • TextField itself is not the input; form attributes belong on TextFieldInput.
  • Prefix and suffix actions change hit area and layout density, so they should be intentional.
  • Error visuals live on the wrapper, but input semantics still belong to the native input.
  • Once masking, password reveal, numeric stepping, or phone formatting become part of the field contract, a specialized field is usually clearer than stretching TextField.

Common Mistakes

  • Passing value, onChange, or name to TextField instead of TextFieldInput.
  • Rendering the wrapper without TextFieldInput.
  • Treating prefix/suffix children as decorative when they change interaction behavior.

Examples

Basic

Basic: work email with a hint

A visible Label points at TextFieldInput through htmlFor, and the hint is linked with aria-describedby. All input attributes — type, name, autoComplete — go on TextFieldInput, not on the wrapper. Ids come from useId, so several fields on one page never collide.

work-email-field.tsx · 24 lines
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function WorkEmailField() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Work email</Label>
      <TextField>
        <TextFieldInput

Controlled/Stateful

Controlled/Stateful: character counter suffix

The parent owns the value, so the counter in the suffix and the preview below stay in sync with every keystroke. maxLength stops typing at the limit, and the counter is linked to the input with aria-describedby.

character-counter-suffix.tsx · 36 lines
import { Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { useId, useState } from 'react';

const MAX_LENGTH = 60;

export default function CharacterCounterSuffix() {
  const id = useId();
  const counterId = `${id}-counter`;
  const [title, setTitle] = useState('Q3 roadmap review');
  const remaining = MAX_LENGTH - title.length;

  return (

Form/Integration

Form/Integration: profile form with FormData

Uncontrolled inputs with name and defaultValue are read with FormData on submit. required and type='email' use native validation, and a reset button restores the default values.

profile-form-submit.tsx · 39 lines
import { Button, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function ProfileFormSubmit() {
  const id = useId();
  const [saved, setSaved] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setSaved(`${data.get('fullName')} <${data.get('email')}>`);
  };

Form/Integration: search with clear and submit

A role='search' form with a leading icon, a clear button that returns focus to the input, and a submit button — all inside one TextField. The input has no visible label, so it gets an aria-label.

search-with-clear-and-submit.tsx · 57 lines
import type { CSSProperties, FormEvent } from 'react';
import { Button, Icon, IconButton, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { SearchIcon, XIcon } from '@vega-ui/icons';
import { useRef, useState } from 'react';
import './examples.css';

export default function SearchWithClearAndSubmit() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [query, setQuery] = useState('');
  const [searched, setSearched] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {

Layout/Overlay

Layout/Overlay: rename inside a dialog

A method='dialog' form closes the dialog on submit, and onSubmit reads the new name with FormData. autoFocus puts the caret straight into the field when the dialog opens.

rename-in-dialog.tsx · 67 lines
import type { CSSProperties, FormEvent } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Label,
  Text,

Error

Error: email validated on blur and submit

The error appears on blur or submit and is re-checked on each keystroke while it is shown, so it clears as soon as the address becomes valid. error only paints the wrapper; aria-invalid and aria-describedby on the input carry the state to screen readers.

email-validation-error.tsx · 50 lines
import { Button, HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

const getEmailError = (value: string) => {
  if (value.trim() === '') return 'Enter an email address to send the invite.';
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Use the full address, like dana@northwind.io.';
  return '';
};

export default function EmailValidationError() {
  const id = useId();
  const messageId = `${id}-message`;

Disabled

Disabled: read-only versus disabled

A read-only input stays focusable and is submitted with the form; a disabled one is skipped by focus and left out of FormData. Submit the form to see which fields arrive.

read-only-and-disabled.tsx · 46 lines
import { Button, HelperText, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function ReadOnlyAndDisabled() {
  const id = useId();
  const [submitted, setSubmitted] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const keys = Array.from(new FormData(event.currentTarget).keys());
    setSubmitted(keys.join(', '));
  };

Edge

Edge: async availability check

A debounced check runs 600 ms after the last keystroke, and the cleanup drops stale timers. The suffix shows a Spinner while checking, and the hint is a polite live region, so the result is announced.

async-username-check.tsx · 60 lines
import { HelperText, Icon, Label, Spinner, TextField, TextFieldInput } from '@vega-ui/react';
import { CheckIcon } from '@vega-ui/icons';
import { useEffect, useId, useState } from 'react';

const TAKEN = ['admin', 'support', 'maria', 'billing'];

interface CheckResult {
  username: string;
  available: boolean;
}

export default function AsyncUsernameCheck() {

Edge: long value between a prefix and a suffix

Static affixes keep their width while the input shrinks (minWidth: 0) and scrolls a long value. The affixes are hidden from screen readers because the hint spells out the full address.

subdomain-prefix-suffix.tsx · 40 lines
import { HelperText, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { useId, useState } from 'react';

const affixStyle = {
  flexShrink: 0,
  whiteSpace: 'nowrap',
  color: 'var(--label-secondary)',
} as const;

export default function SubdomainPrefixSuffix() {
  const id = useId();
  const hintId = `${id}-hint`;

Edge: right-to-left layout

Under dir='rtl' the leading icon moves to the right because the field uses logical insets. The email input keeps dir='ltr', since addresses are always written left to right.

rtl-field.tsx · 31 lines
import { HelperText, Icon, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { SearchIcon } from '@vega-ui/icons';
import { useId } from 'react';

export default function RtlField() {
  const id = useId();

  return (
    <div dir='rtl' lang='he' style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <TextField>
        <Icon size='sm' aria-hidden style={{ flexShrink: 0, marginInlineStart: 'var(--spacing-6)', color: 'var(--label-secondary)' }}>
          <SearchIcon />

Anatomy

Overview

TextField is a compound component with a small composition contract. The wrapper coordinates sizing and visual state, and TextFieldInput provides the real form control.

Required Parts

TextField

Required. Owns shared size, error state, and the visual container.

TextFieldInput

Required. Renders the native <input> and receives actual input props.

Optional Parts

Prefix Or Suffix Children

Optional. Buttons, icons, counters, or status text can be placed before or after the input.

Composition Order

  1. TextField
  2. optional prefix child
  3. TextFieldInput
  4. optional suffix child

Valid Composition Patterns

search-with-clear-and-submit.tsx · 57 lines
import type { CSSProperties, FormEvent } from 'react';
import { Button, Icon, IconButton, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { SearchIcon, XIcon } from '@vega-ui/icons';
import { useRef, useState } from 'react';
import './examples.css';

export default function SearchWithClearAndSubmit() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [query, setQuery] = useState('');
  const [searched, setSearched] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {

Invalid Composition Patterns

Wrapper without TextFieldInput

The component renders, but there is no actual form control.

Input props passed to the wrapper

This breaks form behavior because the wrapper is only a <div>.

Too many competing controls around the input

The field becomes visually dense and hard to use.