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
- :has()
- Oklab and OkLCh — comes from the design tokens
- Logical properties
- :focus-visible
- :where()
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
TextFieldPropsTextFieldInputPropsTextFieldSize = 'sm' | 'md' | 'lg' | string
Minimal Composition
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>
<TextFieldInputRequired Parts
TextField: layout wrapper with size and error contextTextFieldInput: native input element
Optional Parts
- prefix or suffix children such as
Button,IconButton, counters, or status text
Composition Order
TextField- optional leading child
TextFieldInput- 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
TextFielditself is not the input; form attributes belong onTextFieldInput.- 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, ornametoTextFieldinstead ofTextFieldInput. - 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.
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>
<TextFieldInputControlled/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.
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.
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.
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.
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.
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.
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.
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.
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.
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
TextField- optional prefix child
TextFieldInput- optional suffix child
Valid Composition Patterns
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.