Patterns
Field patterns
Form-ready flavors of TextField that bake in the right conventions for each kind of input — type, inputMode, autoComplete, the right icon — plus the little controls you'd expect: a password reveal, a search clear. Light where the convention is light (Email), with real wiring where it's needed (Password, Phone). Each one reads its field from context, so it drops straight into useHaloForm like any other field.
How they’re built
Most just preset props on TextField. The two with controls — Password and Search — hang a real button on Input’s trailingAction slot (the interactive trailing slot, unlike the aria-hidden trailingIcon). Phone has its own wiring so it can normalise the value to E.164 (via libphonenumber) on blur.
Live demo
One form, all six. Submit with an invalid email or phone to see the errors; type in the password to watch the strength meter; type in search to reveal the clear button.
The six
- EmailField
- type=email · inputMode=email · autoComplete=email · mail icon. Pair with
z.string().email(). - PasswordField
- Reveal toggle (flips type, aria-pressed), current- vs new-password autoComplete, lock icon, and an optional strength meter. newPassword turns on the meter + new-password mode.
- SearchField
- type=search, magnifier icon, and an accessible clear (×) button that appears once there’s a value (the native clear is suppressed).
- PhoneField
- type=tel · inputMode=tel · autoComplete=tel · phone icon. Normalises to E.164 on blur via libphonenumber; pair with isE164 (also libphonenumber-backed).
- URLField
- type=url · inputMode=url · link icon. Pair with a url() validator.
- NumberField
- inputMode=decimal (no spinner quirks) with an optional currency / unit prefix in the leading slot.
Phone — E.164
On blur a complete number is normalised to canonical E.164 by toE164(), and isE164() validates it — both go through Google’s libphonenumber (the libphonenumber-js port), so it’s a real validity check, not a regex. Pass defaultCountry to accept national-format input.
Password — strength
The meter scores with scorePassword() — zxcvbn (the maintained @zxcvbn-ts port), lazy-loaded so its dictionaries stay out of the main bundle. The bars fill along a severity color ramp in the official palette's solid fills (the rust-to-patina ramp belongs to the proposed system), and the word always carries the meaning — never the color alone (ADR-0017).
Usage
import { EmailField, PasswordField, PhoneField } from '@halo-compliance/ui'
<form.AppField name="email">{() => <EmailField label="Work email" />}</form.AppField>
<form.AppField name="password">
{() => <PasswordField label="Create a password" newPassword />}
</form.AppField>
<form.AppField name="phone">{() => <PhoneField label="Phone" />}</form.AppField>import { z, isE164 } from '@halo-compliance/ui'
const schema = z.object({
email: z.string().email(),
phone: z.string().refine(isE164, 'Enter an E.164 number'),
})Related
- /text-field — the field these specialise.
- /input — the primitive underneath, and its new trailingAction slot.
- /form — the form layer they plug into.
- /helper-text — the helper / error pattern each renders.