---
title: TextField
description: A text input field with label, validation states, icon support, and FormControl integration
---

```tsx
'use client';

import { TextField } from '@/ui/forms/TextField';

export function TextFieldDemo() {
  return (
    <div className="flex flex-col gap-16">
      <TextField
        name="email"
        label="Email Address"
        description="Enter your email address"
        placeholder="you@example.com"
        className="max-w-sm"
      />
      <TextField
        name="disabled"
        label="Disabled"
        description="This field is disabled"
        placeholder="Disabled input"
        disabled
        className="max-w-sm"
      />
      <TextField
        name="search"
        label="With Icon"
        description="This field has an icon"
        placeholder="Search..."
        icon="PeopleSingle"
        className="max-w-sm"
        validationStatus="success"
      />
      <TextField
        name="error"
        label="Error"
        description="This field has an error message"
        placeholder="Enter your text"
        className="max-w-sm"
        clear="eke"
        validationStatus="error"
        errorMessage="This is an error message"
      />
      <TextField
        name="success"
        label="Success"
        description="This field has a success message"
        placeholder="Enter your text"
        className="max-w-sm"
        validationStatus="success"
      />
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/text-field.json
```

Or manually, copy the following code into `/ui/forms/text-field.tsx`:

```tsx
import clsx from 'clsx';
import { useValue, type UseValueProps } from '../hooks/useValue';
import { useInternalStatus } from '../hooks/useInternalStatus';
import { FormControl, type FormControlProps } from './FormControl';
import { Icon, type IconicNames, type IconicTypes } from '@clubmed/trident-icons';
import { useId } from 'react';

export interface TextFieldProps<Value> extends FormControlProps<Value> {
  description?: string;
  icon?: IconicNames;
  iconType?: IconicTypes;
  errorMessage?: string;
  dataTestId?: string;
  formatter?: UseValueProps<Value>['formatter'];
  hasDropdown?: boolean;
  clear?: string;
  iconFirst?: boolean;
}

export const TextField = <Value = string,>(props: TextFieldProps<Value>) => {
  const internalId = useId();

  const {
    id = internalId,
    name = id,
    label,
    value: initialValue,
    description,
    validationStatus = 'default',
    icon,
    iconType,
    errorMessage,
    disabled = false,
    required = false,
    hideRequiredStar,
    className,
    dataTestId = 'TextField',
    hasDropdown = false,
    clear = '',
    iconFirst = true,
    onChange,
    formatter,
    ...rest
  } = props;

  const { value, setValue } = useValue<Value>({ initialValue, onChange, formatter });
  const internalStatus = useInternalStatus({
    isDisabled: disabled,
    validationStatus,
  });

  const shouldDisplayClearButton =
    !disabled && value !== '' && value !== null && value !== undefined && !!clear;

  return (
    <FormControl
      id={id}
      label={label}
      className={className}
      description={description}
      dataName="TextField"
      dataTestId={dataTestId}
      disabled={disabled}
      required={required}
      hideRequiredStar={hideRequiredStar}
      validationStatus={validationStatus}
      errorMessage={errorMessage}
    >
      <div className="relative">
        <input
          {...rest}
          id={id}
          name={name}
          disabled={disabled}
          required={required}
          value={value as any}
          onChange={(e) => setValue(e.target.value as Value, e.nativeEvent)}
          style={
            {
              '--nbIconsStart': Number(!!icon && iconFirst),
              '--nbIconsEnd':
                Number(!!icon && !iconFirst) +
                Number(
                  (hasDropdown && !shouldDisplayClearButton) ||
                    (shouldDisplayClearButton && internalStatus !== 'error'),
                ) +
                Number(internalStatus === 'error' || internalStatus === 'success'),
            } as React.CSSProperties
          }
          className={clsx(
            'text-b3 rounded-pill w-full inset-ring overflow-hidden px-20 py-12 font-normal outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 ps-[calc(20px+var(--nbIconsStart)*32px)] pe-[calc(20px+var(--nbIconsEnd)*32px)]',
            {
              'inset-ring-middleGrey focus:inset-ring-black active:inset-ring-black':
                internalStatus === 'default',
              'bg-white text-black': internalStatus !== 'disabled',
              'bg-pearl inset-ring-middleGrey': internalStatus === 'disabled',
              'inset-ring-red': internalStatus === 'error',
              'inset-ring-green': internalStatus === 'success',
            },
          )}
          aria-label={name}
        />

        <div
          className={clsx(
            'pointer-events-none absolute inset-0 flex items-center justify-between px-20 py-12',
            {
              'text-grey': internalStatus === 'disabled',
              'text-red': internalStatus === 'error',
              'text-green': internalStatus === 'success',
            },
          )}
        >
          {icon && iconFirst && <Icon name={icon} width="24px" />}

          <span className="ms-auto flex gap-x-8">
            {internalStatus === 'error' &&
              (shouldDisplayClearButton ? (
                <button
                  className="pointer-events-auto cursor-pointer flex outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
                  onClick={(e) => setValue('' as Value, e.nativeEvent)}
                  aria-label={clear}
                  type="reset"
                >
                  <Icon name="CrossDefault" width="24px" type={iconType} />
                </button>
              ) : (
                <Icon name="CrossDefault" width="24px" type={iconType} />
              ))}

            {internalStatus === 'success' && (
              <Icon name="CheckDefault" width="24px" type={iconType} />
            )}
            {(hasDropdown || (shouldDisplayClearButton && internalStatus !== 'error')) &&
              (shouldDisplayClearButton ? (
                <button
                  className="pointer-events-auto cursor-pointer flex outline-none focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2"
                  onClick={(e) => setValue('' as Value, e.nativeEvent)}
                  aria-label={clear}
                  type="reset"
                >
                  <Icon name="CrossDefault" className="text-black" width="24px" />
                </button>
              ) : (
                <Icon name="ArrowDefaultDown" className="text-black" width="24px" />
              ))}
            {!iconFirst && icon && <Icon name={icon} width="24px" />}
          </span>
        </div>
      </div>
    </FormControl>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { TextField } from '@/docs/lib/ui/forms/text-field';

export function Component() {
  const [email, setEmail] = useState('');

  return (
    <TextField
      label="Email Address"
      description="We'll never share your email"
      placeholder="you@example.com"
      value={email}
      onChange={(_, value) => setEmail(value)}
      icon="MailDefault"
      required
      validationStatus="default"
      className="max-w-sm"
    />
  );
}
```

## API Reference

### TextField

| Prop               | Type                                    | Default        | Description                                                                              |
| ------------------ | --------------------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `label`            | `string`                                | `undefined`    | The label text displayed above the input field                                           |
| `description`      | `string`                                | `undefined`    | Helper text displayed below the label to provide additional context                      |
| `id`               | `string`                                | auto-generated | The HTML id attribute for the input element. Auto-generated if not provided              |
| `name`             | `string`                                | `id`           | The name attribute for the input. Defaults to the id value                               |
| `value`            | `Value`                                 | `undefined`    | The controlled value of the input field. Supports generic type parameter                 |
| `onChange`         | `(event: Event, value: Value) => void`  | `undefined` | Callback fired when input value changes. Receives event and new value        |
| `disabled`         | `boolean`                               | `false`        | If true, disables the input field and applies disabled styling (grey background)         |
| `required`         | `boolean`                               | `false`        | If true, marks the field as required and displays a red asterisk next to the label       |
| `hideRequiredStar` | `boolean`                               | `false`        | If true, hides the required asterisk even when required is true                          |
| `validationStatus` | `"default" \| "error" \| "success"`     | `"default"`    | The validation state of the field. Changes border color and displays validation icons    |
| `errorMessage`     | `string`                                | `undefined`    | Error message displayed below the field when validationStatus is "error"                 |
| `icon`             | `IconicNames`                           | `undefined`    | Optional icon displayed on the left side of the input field                              |
| `iconType`         | `IconicTypes`                           | `undefined`    | The icon type variant for validation icons (error/success indicators)                    |
| `formatter`        | `(value: Value) => Value`               | `undefined`    | Optional function to format/transform the input value before updating state              |
| `dataTestId`       | `string`                                | `"TextField"`  | Custom test ID for testing purposes                                                      |
| `className`        | `string`                                | `undefined`    | Additional CSS classes to apply to the FormControl container                             |
| `hasDropdown`      | `boolean`                               | `false`        | If true, displays a dropdown icon on the right side of the input field                  |
| `clear`            | `string`                                | `""`           | Text for the clear button aria-label (and controls whether such a button exists)                                                     |
| `iconFirst`        | `boolean`                               | `true`         | If true, displays the icon on the left side of the input field                           |
| `...rest`          | `InputHTMLAttributes<HTMLInputElement>` | -              | All standard HTML input attributes (placeholder, type, autoComplete, etc.) are supported |

## Notes

- **FormControl Integration**: TextField is built on top of FormControl, providing consistent label, description, and error message layout across form fields

- **Generic Value Type**: Supports generic type parameter for type-safe value handling. By default uses `string`, but can be customized with `TextField<number>` or other types

- **Automatic State Management**: Uses internal `useValue` hook to manage state. Can be used as controlled or uncontrolled component

- **Validation States**: Three validation states available:

  - `default`: Standard grey border with black focus
  - `error`: Red border with cross icon on the right
  - `success`: Green border with checkmark icon on the right

- **Validation Icons**: Error (cross) and success (checkmark) icons automatically appear on the right side when validation status changes

- **Icon Support**: Optional icon can be displayed on the left side of the input using the `icon` prop with any IconicNames value

- **Custom Formatter**: The `formatter` prop allows transformation of input values before they're passed to onChange (e.g., uppercase conversion, number formatting)

- **Disabled State**: When disabled, the input displays grey background (`bg-pearl`) and grey text, and user interaction is blocked

- **Rounded Pill Design**: Input has rounded pill styling (`rounded-pill`) with consistent padding

- **Dynamic Padding**: Automatically adjusts padding based on presence of icons:

  - Left padding increases when icon is present
  - Right padding increases when validation icons are shown

- **Automatic ID Generation**: If no id is provided, React's useId hook generates a unique ID for accessibility

- **Accessibility**: Input includes aria-label attribute set to the field name for screen reader support

- **Client Component**: This is a client component (uses React hooks) and must be used in client-side contexts

- **Data Attributes**: Includes `data-name="TextField"` for testing and debugging purposes
