# Field: Text Input (category: text)

URL: https://docs.forms.saastro.io/docs/text

<FieldPreview name="text" description="Interactive demo of text input fields" />

## Overview

The text field family is the most basic input in `@saastro/forms`. Six field types share the same renderer — the field's `type` is passed straight through to the native `<input>` element's `type` attribute:

| `type`     | Native input              | Notes                                                                                  |
| ---------- | ------------------------- | -------------------------------------------------------------------------------------- |
| `text`     | `<input type="text">`     | Standard single-line text                                                               |
| `email`    | `<input type="email">`    | Email keyboard on mobile; add `.email()` for format validation                          |
| `tel`      | `<input type="tel">`      | Telephone keypad on mobile; no built-in format validation                               |
| `url`      | `<input type="url">`      | URL keyboard on mobile; add `.url()` for format validation                              |
| `password` | `<input type="password">` | Masks the entered value                                                                 |
| `number`   | `<input type="number">`   | Numeric keyboard; the value is a **string** at runtime (see [Number type](#number-type)) |

## Usage

### JSON Configuration

```json
{
  "type": "text",
  "label": "Your Name",
  "placeholder": "Enter your name",
  "schema": {
    "required": true,
    "minLength": 2
  }
}
```

### FormBuilder API

```tsx

const config = FormBuilder.create('contact-form')
  .addField('name', (f) =>
    f
      .type('text')
      .label('Your Name')
      .placeholder('Enter your name')
      .required('Name is required')
      .minLength(2, 'Name must be at least 2 characters'),
  )
  .addStep('main', ['name'])
  .build();
```

## Email Type

Use `type: 'email'` together with the `.email()` validation rule for format validation:

```tsx
.addField('email', (f) =>
  f.type('email')
    .label('Email Address')
    .placeholder('you@example.com')
    .required()
    .email('Please enter a valid email')
)
```

## Tel Type

Use `type: 'tel'` for phone number inputs:

```tsx
.addField('phone', (f) =>
  f.type('tel')
    .label('Phone Number')
    .placeholder('+1 (555) 123-4567')
    .required()
)
```

## URL Type

Use `type: 'url'` together with the `.url()` validation rule:

```tsx
.addField('website', (f) =>
  f.type('url')
    .label('Website')
    .placeholder('https://example.com')
    .url('Please enter a valid URL')
)
```

## Password Type

Use `type: 'password'` for masked input. Combine it with one of the built-in validation presets (`password-simple`, `password-medium`, `password-strong`):

```tsx
.addField('password', (f) =>
  f.type('password')
    .label('Password')
    .required('Password is required')
    .preset('password-strong')
)
```

## Number Type

Use `type: 'number'` for numeric input. Because it renders a native number input, the field value is a **string** at runtime. Use `z.coerce.number()` for validation and the `toNumber` transform to receive a number in the submit payload:

```tsx

.addField('quantity', (f) =>
  f.type('number')
    .label('Quantity')
    .placeholder('0')
    .schema(z.coerce.number().min(1, 'Enter at least 1'))
    .transform('toNumber')
)
```

## Props

| Prop           | Type                                                       | Default | Description                                |
| -------------- | ---------------------------------------------------------- | ------- | ------------------------------------------ |
| `type`         | `'text' \| 'email' \| 'tel' \| 'url' \| 'password' \| 'number'` | -       | Input type (required)                      |
| `label`        | `string`                                                   | -       | Label shown above the input                |
| `placeholder`  | `string`                                                   | -       | Placeholder text                           |
| `schema`       | `z.ZodType \| ValidationRules`                             | -       | Validation: a Zod schema or serializable JSON rules (required — fluent methods like `.required()` or `.email()` populate it for you) |
| `hideLabel`    | `boolean`                                                  | `false` | Hide the label visually                    |
| `helperText`   | `string`                                                   | -       | Help text shown below input                |
| `tooltip`      | `string`                                                   | -       | Tooltip text for additional info           |
| `icon`         | `ReactNode`                                                | -       | Icon element to display                    |
| `size`         | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'`                     | `'md'`  | Input size variant                         |
| `disabled`     | `boolean \| function \| ConditionGroup`                    | `false` | Disable the input                          |
| `hidden`       | `boolean \| function \| ConditionGroup \| Responsive`      | `false` | Hide the field                             |
| `readOnly`     | `boolean \| function \| ConditionGroup`                    | `false` | Make input read-only                       |
| `columns`      | `Partial<Record<Breakpoint, number>>`                      | -       | Grid columns by breakpoint                 |
| `autocomplete` | `string`                                                   | -       | HTML autocomplete attribute                |

> **Default placeholders.** When `placeholder` is omitted, the library falls back to built-in English defaults (`'Enter an email...'` for `email`, `'Enter a phone number...'` for `tel`, `'Enter a value...'` for the rest). Set a `placeholder` per field or override the defaults globally with `setDefaultMessages` — see the [Internationalization guide](/docs/i18n).

## Validation

### Declarative (JSON-serializable)

```json
{
  "schema": {
    "required": true,
    "requiredMessage": "This field is required",
    "minLength": 2,
    "minLengthMessage": "Minimum 2 characters",
    "maxLength": 100,
    "pattern": "^[a-zA-Z]+$",
    "patternMessage": "Only letters allowed"
  }
}
```

### Zod Schema

```tsx

.addField('name', (f) =>
  f.type('text')
    .label('Name')
    .schema(z.string().min(2).max(100))
)
```

### Fluent Validation API

```tsx
.addField('name', (f) =>
  f.type('text')
    .label('Name')
    .required('Name is required')
    .minLength(2, 'Too short')
    .maxLengthValidation(100, 'Too long')
)
```

## Styling

### Custom Classes

```tsx
.addField('name', (f) =>
  f.type('text')
    .label('Name')
    .classNames({
      wrapper: 'col-span-6',
      input: 'border-blue-500',
      label: 'text-blue-700',
      error: 'text-red-600',
    })
)
```

### Responsive Layout

```tsx
.addField('name', (f) =>
  f.type('text')
    .label('Name')
    .columns({ default: 12, md: 6, lg: 4 })
)
```

Per-field column spans take effect when the form uses manual layout mode (e.g. `FormBuilder.create('id').layout('manual').columns(12)`). In `auto` mode the grid is adaptive and per-field `columns` are ignored — see the [Layout guide](/docs/layout).

## Related Fields

- [Textarea](/docs/textarea) - Multi-line text input
- [Input Group](/docs/input-group) - Text input with prefix/suffix addons

## Accessibility

- Renders through your injected `Input` component with the native `type` attribute, so browser behaviors (email keyboard on mobile, password masking, numeric keypad) work out of the box
- The field container receives `data-invalid` when validation fails
- Validation errors render through the injected `FieldError` component; helper text through `FieldDescription`
- Label association and ARIA attributes (`htmlFor`, `aria-invalid`, `aria-describedby`) come from the injected component set — shadcn/ui's form primitives wire these automatically
