Custom Form Controls with useField0%

Custom Form Controls with useField

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Custom Form Controls with useField

Standard HTML <input> and <select> elements rarely satisfy enterprise UI standards. Design systems demand bespoke controls: custom multi-select tag pickers, star rating widgets, masked phone inputs, and toggle switches. Formik's useField hook connects arbitrary custom React components directly to Formik's validation and state engine.


1. Anatomy of useField

The useField hook accepts a field name (or props object) and returns a tuple containing three items:

TSX
const [field, meta, helpers] = useField(name);
  • field: Props to spread onto standard inputs (name, value, onChange, onBlur).
  • meta: Metadata regarding field state (value, error, touched, initialValue).
  • helpers: Imperative setter functions (setValue, setTouched, setError) to update Formik state programmatically from custom UI events.

2. Building a Custom Switch Toggle Control

TSX
import React from "react";
import { useField } from "formik";
 
interface SwitchToggleProps {
name: string;
label: string;
description?: string;
}
 
export function SwitchToggle({ name, label, description }: SwitchToggleProps) {
// Bind custom toggle directly to Formik
const [field, meta, helpers] = useField<boolean>(name);
 
const isChecked = Boolean(field.value);
 
const handleToggle = () => {
helpers.setValue(!isChecked);
helpers.setTouched(true);
};
 
return (
<div className="flex items-center justify-between p-4 bg-slate-900 border border-slate-800 rounded-xl">
<div>
<label className="text-sm font-semibold text-white block">{label}</label>
{description && <p className="text-xs text-slate-400">{description}</p>}
{meta.touched && meta.error && (
<span className="text-xs text-rose-400 block mt-1">{meta.error}</span>
)}
</div>
 
{/* Custom accessible switch primitive */}
<button
type="button"
role="switch"
aria-checked={isChecked}
onClick={handleToggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-500 ${
isChecked ? "bg-cyan-600" : "bg-slate-700"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
isChecked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
);
}

3. Building a Multi-Select Tag Input

TSX
import React, { useState } from "react";
import { useField } from "formik";
 
interface TagInputProps {
name: string;
label: string;
placeholder?: string;
}
 
export function TagInput({ name, label, placeholder = "Type and press Enter..." }: TagInputProps) {
const [field, meta, helpers] = useField<string[]>(name);
const [inputValue, setInputValue] = useState("");
 
const tags = field.value || [];
 
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && inputValue.trim()) {
e.preventDefault();
if (!tags.includes(inputValue.trim())) {
helpers.setValue([...tags, inputValue.trim()]);
}
setInputValue("");
helpers.setTouched(true);
}
};
 
const removeTag = (indexToRemove: number) => {
helpers.setValue(tags.filter((_, idx) => idx !== indexToRemove));
helpers.setTouched(true);
};
 
return (
<div className="space-y-1">
<label className="block text-xs text-slate-400 font-medium">{label}</label>
 
<div className="flex flex-wrap gap-2 p-2 bg-slate-900 border border-slate-800 rounded-lg min-h-[44px] items-center">
{tags.map((tag, idx) => (
<span
key={idx}
className="flex items-center gap-1 bg-cyan-950/60 text-cyan-300 border border-cyan-800 text-xs px-2.5 py-1 rounded-md"
>
{tag}
<button
type="button"
onClick={() => removeTag(idx)}
className="text-cyan-400 hover:text-white font-bold ml-1"
>
×
</button>
</span>
))}
 
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={tags.length === 0 ? placeholder : ""}
className="flex-1 bg-transparent text-sm text-white focus:outline-none min-w-[120px]"
/>
</div>
 
{meta.touched && meta.error && (
<div className="text-xs text-rose-400 mt-1">{meta.error}</div>
)}
</div>
);
}

4. Seamless Formik Consumption

Because SwitchToggle and TagInput manage their own Formik subscriptions internally via useField, consuming them in forms is completely clean:

TSX
<Formik initialValues={{ enable2FA: false, skillTags: ["React", "TypeScript"] }} onSubmit={...}>
<Form className="space-y-4">
<SwitchToggle
name="enable2FA"
label="Two-Factor Authentication"
description="Require TOTP hardware token on login"
/>
<TagInput name="skillTags" label="Developer Competencies" />
<button type="submit">Save Settings</button>
</Form>
</Formik>

Interactive Quiz

Practice Quiz

Question 1
What does the useField hook return in Formik?
Question 2
How do you programmatically update a custom component's value in Formik without a standard onChange event?
Question 3
Why should helpers.setTouched(true) be called when a user interacts with a custom control?
Question 4
Can custom controls built with useField accept generic types (e.g. useField<string[]>(name))?
Question 5
What is the primary architectural advantage of building design system inputs with useField?
Next Lesson

High-Performance Form Optimization Comparison

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext