@letar/forms

Smart Autofill

Automatic HTML autocomplete attributes for better UX and accessibility

Overview

Correct autocomplete attributes improve form conversion by 30% (Google) and satisfy WCAG 1.3.5 for personal data fields. @letar/forms automatically sets autocomplete based on field names — no configuration needed.

Full example

The complete sandbox example, read directly from the form-develop-app source at build time.

apps/form-develop-app/src/app/autofill-demo/page.tsx
'use client'

import { Code, Heading, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useEffect, useRef, useState } from 'react'
import { z } from 'zod/v4'
import { DemoPageLayout } from '../_components'

const ContactSchema = z
  .object({
    firstName: z
      .string()
      .min(1, 'Обязательное')
      .meta({ ui: { title: 'Имя' } }),
    lastName: z
      .string()
      .min(1, 'Обязательное')
      .meta({ ui: { title: 'Фамилия' } }),
    email: z
      .string()
      .email('Некорректный')
      .meta({ ui: { title: 'Email' } }),
    phone: z
      .string()
      .optional()
      .meta({ ui: { title: 'Телефон' } }),
    company: z
      .string()
      .optional()
      .meta({ ui: { title: 'Компания' } }),
    address: z
      .string()
      .optional()
      .meta({ ui: { title: 'Адрес' } }),
    city: z
      .string()
      .optional()
      .meta({ ui: { title: 'Город' } }),
    postalCode: z
      .string()
      .optional()
      .meta({ ui: { title: 'Индекс' } }),
    country: z
      .string()
      .optional()
      .meta({ ui: { title: 'Страна' } }),
    // Поле без автоопределения — проверяем что не ставится
    notes: z
      .string()
      .optional()
      .meta({ ui: { title: 'Заметки' } }),
    // Явное отключение через meta
    secretCode: z
      .string()
      .optional()
      .meta({ ui: { title: 'Секретный код', autocomplete: 'off' } }),
  })
  .strip()

/**
 * Компонент для отображения autocomplete атрибутов из DOM
 */
function AutocompleteInspector() {
  const [attrs, setAttrs] = useState<Array<{ name: string; autocomplete: string | null }>>([])
  const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  useEffect(() => {
    // Даём время на рендер полей
    timerRef.current = setTimeout(() => {
      const inputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(
        'input[data-field-name], textarea[data-field-name]',
      )
      const result = Array.from(inputs).map((input) => ({
        name: input.getAttribute('data-field-name') ?? '?',
        autocomplete: input.getAttribute('autocomplete'),
      }))
      setAttrs(result)
    }, 500)

    return () => clearTimeout(timerRef.current)
  }, [])

  if (attrs.length === 0) {
    return null
  }

  return (
    <VStack gap={1} align="stretch" p={4} bg="bg.subtle" borderRadius="md">
      <Heading size="sm" mb={2}>
        Autocomplete атрибуты в DOM
      </Heading>
      {attrs.map((a) => (
        <Text key={a.name} fontSize="sm">
          <Code>{a.name}</Code> →{' '}
          <Code colorPalette={a.autocomplete ? 'green' : 'gray'}>{a.autocomplete ?? 'не установлен'}</Code>
        </Text>
      ))}
    </VStack>
  )
}

export default function AutofillDemoPage() {
  return (
    <DemoPageLayout
      title="Smart Autofill Demo"
      description="Автоматическое проставление autocomplete атрибутов по имени поля. Проверяйте в DevTools."
    >
      <Form
        schema={ContactSchema}
        initialValue={{ firstName: '', lastName: '', email: '' }}
        onSubmit={async (data) => alert(JSON.stringify(data, null, 2))}
      >
        <VStack gap={4} align="stretch">
          <Form.Field.String name="firstName" />
          <Form.Field.String name="lastName" />
          <Form.Field.String name="email" />
          <Form.Field.String name="phone" />
          <Form.Field.String name="company" />
          <Form.Field.String name="address" />
          <Form.Field.String name="city" />
          <Form.Field.String name="postalCode" />
          <Form.Field.String name="country" />
          <Form.Field.String name="notes" />
          <Form.Field.String name="secretCode" />
          <Form.Button.Submit>Отправить</Form.Button.Submit>
        </VStack>

        <AutocompleteInspector />
      </Form>
    </DemoPageLayout>
  )
}

How It Works

Fields named email, phone, firstName, etc. automatically receive the correct autocomplete attribute:

<Form schema={Schema} initialValue={data} onSubmit={save}>
  <Form.Field.String name="email" /> {/* autocomplete="email" */}
  <Form.Field.String name="firstName" /> {/* autocomplete="given-name" */}
  <Form.Field.Phone name="phone" /> {/* autocomplete="tel" */}
  <Form.Field.Password name="password" /> {/* autocomplete="current-password" */}
  <Form.Field.String name="city" /> {/* autocomplete="address-level2" */}
</Form>

No props needed — it just works.

Supported Fields

Field name patternautocomplete value
email, e-mailemail
phone, tel, mobiletel
firstName, first_namegiven-name
lastName, last_name, surnamefamily-name
name, fullNamename
passwordcurrent-password
newPassword, confirmPasswordnew-password
address, streetstreet-address
cityaddress-level2
state, regionaddress-level1
zip, postalCodepostal-code
countrycountry-name
company, organizationorganization
usernameusername

Override via Schema

Use .meta({ ui: { autocomplete } }) to override auto-detection:

const Schema = z.object({
  // Override: use street-address instead of default
  deliveryAddress: z.string().meta({
    ui: { title: 'Delivery Address', autocomplete: 'street-address' },
  }),

  // Disable autofill for this field
  secretCode: z.string().meta({
    ui: { title: 'Secret Code', autocomplete: 'off' },
  }),
})

Override via Props

The autoComplete prop always wins:

<Form.Field.String name="email" autoComplete="username" />

Priority

  1. autoComplete prop (highest)
  2. .meta({ ui: { autocomplete } }) from Zod schema
  3. Auto-detection from field name

Nested Fields

For nested paths like address.city, auto-detection uses the last segment (city):

<Form.Group name="billing">
  <Form.Field.String name="city" /> {/* autocomplete="address-level2" */}
  <Form.Field.String name="zip" /> {/* autocomplete="postal-code" */}
</Form.Group>

Affected Components

Auto-detection works on:

  • Form.Field.String
  • Form.Field.Password
  • Form.Field.Textarea
  • Form.Field.Phone (always tel)

Live Example

Try the interactive example on forms-example.letar.best.

On this page