Skip to content

TextField

TextField wraps a text input with label, helper, and validation text. It is intended for text input types, such as text, number, email, password, tel, etc.
Figma logo
  • We suggest using Grid to lay out your forms. We currently suggest a rowGap of 3 spacing units. A suggested form width is still being established, but 632 is a good suggestion for the time being.
  • We suggest building forms with Formik to leverage its built-in support for state management and client side validation via Yup.
  • In the UI, we do not mark required fields; rather, we mark those that are optional.
    • Add the required attribute on a field to require valid input
    • You can customize the default validation text using a validationSchema via Yup.
    • Provide a string to the optionalText prop to mark optional fields.

A basic TextField implementation using Formik and Yup to validate a required email text input.

A basic TextField implementation includes an input with a required label. Descriptive helperText can be added.

Disabled states should be used with caution.

Content can be prepended or appended

Mx.
Esquire
Mx.
Esquire

import React, { useState } from 'react'
import { Button, TextField, visuallyHidden } from '@gusto/workbench'
import { Search } from '@gusto/workbench-icons'
import { FormLayout } from '@gusto/workbench-layouts'
export const WithBeforeAfterContent = () => {
const [type, setType] = useState('password')
const [value, setValue] = useState('payroll')
return (
<FormLayout
fields={
<>
<TextField
label="Full name"
autoComplete="name"
name="name"
before="Mx."
/>
<TextField
label="Full name"
autoComplete="name"
name="name"
after="Esquire"
/>
<TextField
label="Full name"
autoComplete="name"
name="name"
before="Mx."
after="Esquire"
/>
<TextField
label="Password"
autoComplete="current-password"
name="password"
type={type}
after={
// Clickable or interactable elements must override the
// default `pointer-events: none;` by setting `pointer-events: auto;`.
// You may also want to remove the element from the tab order using `tabIndex={-1}`
<Button
variant="link"
style={{ pointerEvents: 'auto' }}
onClick={() =>
setType(type === 'password' ? 'text' : 'password')
}
>
{type === 'password' ? 'show' : 'hide'}{' '}
<span className={visuallyHidden}>password</span>
</Button>
}
defaultValue="password1!"
/>
<TextField
type="search"
name="search"
label="Search"
before={<Search />}
value={value}
onChange={e => setValue(e.target.value)}
after={
value ? (
<Button
onClick={() => setValue('')}
style={{ pointerEvents: 'auto' }}
variant="link"
>
clear
</Button>
) : null
}
/>
</>
}
/>
)
}

The CharacterCount component can be added to fields.

Any valid HTML attribute for the <input/> tag can be passed through TextField. In this example, we leverage various intrinsic HTML attributes to alter the behavior of the <input/>.

Setting the appropriate type on the input provides the browser with a hint as to which virtual keyboard would be most helpful when entering data in this field (relevant in mobile devices).The size and placeholder, min, and max are commonly used to set parameters for the data the field can accept.

We recommend against using input masking. Instead, we suggest using client side validation that accepts almost any format and applies a mask-like format on blur.

Optional fields should be called out in the UI.

It may sometimes be necessary to hide labels, but label text must still be made available for screen readers. This example uses visuallyHidden to visually hide the label.

React props
NameTypeDefaultDescription
after  
ReactNode
-Content shown after the inpinputut value. By default this content is not interactive and will ignore pointer events. Any interactive elements must be styled with pointer-events: auto;
before  
ReactNode
-Content shown before the input value. By default this content is not interactive and will ignore pointer events. Any interactive elements must be styled with pointer-events: auto;
children  
ReactNode
-The content of the component. This content will be rendered as a sibling of the native input element.
component  
ReactNode
-Component override for the input element.
counter  
ReactNode
-Indicator of a limit placed on a field, e.g. maximum character count or remaining hours.
fit  
contentcontainer
-Determines the point of reference for the width of the component. If set to content, the size will be set by the size of the input. If set to container, the size will expand to fit the containing element.
helperText  
ReactNode
-A brief description or hint for the input; can be used to link to an external resource.
id  
string
-The ID of the input element.
invalid  
boolean
-If true, the field will be shown in an invalid state.
label  Required
string
-Label associated with the input.
name  Required
string
-Name of the input. Submitted with the form as part of a name/value pair.
optionalText  
string
-Text used to indicate that a field is not required.
validationText  
ReactNode
-Validation message associated with the input.
  • Always provide a label for each input. It can be visually hidden for UI where labels are superfluous to sighted users.
  • Avoid changing tabindex as this will affect the ability for keyboard users to navigate sequentially through the form.
  • Note that setting the maxLength for an input means that it will stop accepting input once that value is reached. However, some screen readers will continue to announce characters if the user continues typing. This means that users relying on screen readers may not realize that characters are no longer being entered. To avoid this, we recommend passing
    NaN characters left
    to the counter prop.

The following testing snippet(s) offer suggestions for testing the component using React Testing LibraryExternal link with occasional help from JestExternal link.

// BASIC TESTS
render(<TextField label="Label" helperText="Helper text" />);
const input = screen.getByLabelText('Label');
expect(input).toHaveAccessibleDescription('Helper text');
userEvent.type(input, 'Hello');
expect(input).toHaveDisplayValue('Hello');
// TESTING VALIDITY
render(<TextField label="Label" invalid validationText="Validation text" />);
const input = screen.getByLabelText('Label');
expect(input).toBeInvalid(); // or expect(input).not.toBeValid();
expect(input).toHaveAccessibleDescription('Validation text');
// TESTING MULTIPLE DESCRIPTIVE ELEMENTS
render(
<TextField
label="Label"
optionalText="(optional)"
helperText="Helper text"
validationText="Validation text"
/>,
);
const input = screen.getByLabelText('Label');
expect(input).toHaveAccessibleDescription(expect.stringContaining('(optional)'));
expect(input).toHaveAccessibleDescription(expect.stringContaining('Helper text'));
expect(input).toHaveAccessibleDescription(expect.stringContaining('Validation text'));
// or all at once:
expect(input).toHaveAccessibleDescription('(optional) Helper text Validation text');
// TESTING BEFORE AND AFTER
render(<TextField label="Label" before="Before" after="After" />);
const inputContainer = screen.getByLabelText('Label').parentElement as HTMLDivElement;
expect(container).toHaveTextContent('Before');
expect(container).toHaveTextContent('After');