Text Field

Text Input

A single-line input field rendered as a native HTML input. Supports the text, email, tel, url, password, and number input types.

stable
input text email tel url password number form

Interactive demo of text input fields

Your name as it appears on official documents

We'll never share your email

Optional - for urgent matters only

/**
 * Text Field Demo - Interactive example of text input fields
 */

import { Form, FormBuilder } from '@saastro/forms';

import { FormProvider } from '@/components/FormProvider';
import { TooltipProvider } from '@/components/ui/tooltip';

const config = FormBuilder.create('text-demo')
  .layout('manual')
  .columns(12)
  .addField('name', (f) =>
    f
      .type('text')
      .label('Your Name')
      .placeholder('Enter your full name')
      .helperText('Your name as it appears on official documents')
      .required('Name is required')
      .minLength(2, 'Name must be at least 2 characters')
      .columns({ default: 12, md: 6 }),
  )
  .addField('email', (f) =>
    f
      .type('email')
      .label('Email Address')
      .placeholder('[email protected]')
      .helperText("We'll never share your email")
      .required('Email is required')
      .email('Please enter a valid email address')
      .columns({ default: 12, md: 6 }),
  )
  .addField('phone', (f) =>
    f
      .type('tel')
      .label('Phone Number')
      .placeholder('+1 (555) 123-4567')
      .helperText('Optional - for urgent matters only')
      .optional()
      .columns({ default: 12 }),
  )
  .addStep('main', ['name', 'email', 'phone'])
  .build();

export default function TextDemo() {
  const handleSubmit = (data: Record<string, unknown>) => {
    console.log('Form submitted:', data);
    alert('Form submitted! Check console for data.');
  };

  return (
    <TooltipProvider>
      <FormProvider>
        <Form config={config} onSubmit={handleSubmit} className="space-y-4" />
      </FormProvider>
    </TooltipProvider>
  );
}

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:

typeNative inputNotes
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)

Usage

JSON Configuration

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

FormBuilder API

import { FormBuilder } from '@saastro/forms';

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:

.addField('email', (f) =>
  f.type('email')
    .label('Email Address')
    .placeholder('[email protected]')
    .required()
    .email('Please enter a valid email')
)

Tel Type

Use type: 'tel' for phone number inputs:

.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:

.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):

.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:

import { z } from 'zod';

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

Props

PropTypeDefaultDescription
type'text' | 'email' | 'tel' | 'url' | 'password' | 'number'-Input type (required)
labelstring-Label shown above the input
placeholderstring-Placeholder text
schemaz.ZodType | ValidationRules-Validation: a Zod schema or serializable JSON rules (required — fluent methods like .required() or .email() populate it for you)
hideLabelbooleanfalseHide the label visually
helperTextstring-Help text shown below input
tooltipstring-Tooltip text for additional info
iconReactNode-Icon element to display
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Input size variant
disabledboolean | function | ConditionGroupfalseDisable the input
hiddenboolean | function | ConditionGroup | ResponsivefalseHide the field
readOnlyboolean | function | ConditionGroupfalseMake input read-only
columnsPartial<Record<Breakpoint, number>>-Grid columns by breakpoint
autocompletestring-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.

Validation

Declarative (JSON-serializable)

{
  "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

import { z } from 'zod';

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

Fluent Validation API

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

Styling

Custom Classes

.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

.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.

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