Defines patterns for building multi-step forms (wizards for creation, tabs for editing) with validation.
Install
mkdir -p .claude/skills/multi-step-form && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/19163" && unzip -o skill.zip -d .claude/skills/multi-step-form && rm skill.zipInstalls to .claude/skills/multi-step-form
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Guide for building multi-step forms with sequential wizard creation and tab-based editing. Use when creating complex forms that span multiple sections, require per-step validation, or involve related entities (e.g., parent + children records). Based on the SME create/edit pattern.Key capabilities
- →Build sequential wizard forms for creation flows
- →Implement tab-based interfaces for editing forms
- →Apply per-step validation before advancing in a wizard
- →Manage form state for logical sections
- →Convert data between camelCase and snake_case for API interaction
- →Fetch dynamic select options from a configs API
How it works
The skill guides the construction of multi-step forms by defining distinct strategies for creation (sequential wizard) and editing (tab-based interface), enforcing per-step validation and specific data handling patterns.
Inputs & outputs
When to use multi-step-form
- →Build a wizard flow
- →Implement tabbed editing
- →Apply per-step form validation
About this skill
Multi-Step Form Pattern
This skill defines how to build multi-step forms in this codebase. Follow these patterns when creating forms that collect data across multiple sections or involve related entities.
Architecture Overview
Multi-step forms use two distinct strategies:
| Mode | Pattern | Navigation | Save Behavior |
|---|---|---|---|
| Create | Sequential wizard with Progress bar | Next/Previous buttons, per-step validation gates | Single final submission creates all entities |
| Edit | Tab-based interface with Tabs | Click any tab freely | Each tab saves independently |
Key Principles
- Create = wizard, Edit = tabs - Creation walks users through steps in order. Editing lets users jump to any section.
- Per-step validation before advancing - Never let the user proceed to the next step with invalid data.
- Single error state - Use one
errors: Record<string, string>for the entire form, cleared on step change or successful validation. - forwardRef + useImperativeHandle - Expose
handleSubmitto the parentCrudPagecomponent. On create forms, this either advances to next step or submits on the final step. - Sequential API calls on create - Create the parent entity first, then child entities using the parent's returned ID.
- Independent saves on edit - Each tab has its own save handler. Only the active tab saves when the user clicks Save.
- Double submission guard - Use an
isSubmittingflag to prevent duplicate submissions. - Snake/camel case conversion - Frontend uses camelCase, backend uses snake_case. Convert with
snakefiyKeys()on submit, manual mapping on load.
File Structure
For an entity called {Entity}:
resources/js/pages/{Entity}/sections/
{Entity}CreateForm.tsx # Sequential wizard (forwardRef)
{Entity}EditForm.tsx # Wrapper that fetches related data
{Entity}EditFormSimple.tsx # Tab-based form (forwardRef)
edit-tabs/
{Entity}Edit{Section}Tab.tsx # One component per tab
Form State Management
Each logical section gets its own useState:
// Step 1
const [businessData, setBusinessData] = useState<EntityCreateData>({...defaults});
// Step 2
const [ownerData, setOwnerData] = useState<OwnerData>({...defaults});
// Step 3
const [detailsData, setDetailsData] = useState<DetailsData>({...defaults});
// Shared
const [errors, setErrors] = useState<Record<string, string>>({});
const [currentStep, setCurrentStep] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
Update with spread: setBusinessData({ ...businessData, fieldName: value }).
For array fields (multi-select), use add/remove helpers:
const addItem = (value: string) => {
if (value && !data.items.includes(value)) {
setData({ ...data, items: [...data.items, value] });
}
};
const removeItem = (value: string) => {
setData({ ...data, items: data.items.filter(v => v !== value) });
};
Step Definitions
Define steps as a constant array:
const STEPS = [
{ id: 1, name: 'Business Information', description: 'Basic details' },
{ id: 2, name: 'Primary Owner', description: 'Owner information' },
{ id: 3, name: 'Formalization', description: 'Status details' },
{ id: 4, name: 'Additional Members', description: 'Optional team members' },
{ id: 5, name: 'Review', description: 'Review and submit' },
];
Validation Pattern
Validate per-step before allowing navigation:
const handleNext = () => {
let isValid = true;
if (currentStep === 1) isValid = validateStep1();
else if (currentStep === 2) isValid = validateStep2();
// ...
if (isValid && currentStep < STEPS.length) {
setCurrentStep(currentStep + 1);
setErrors({});
}
};
Each validation function builds an errors object and returns a boolean:
const validateStep1 = (): boolean => {
const newErrors: Record<string, string> = {};
// Required fields
if (!data.name?.trim()) newErrors.name = 'Name is required';
// Format validation (only if value present)
if (data.phone && !validateMalawiPhone(data.phone)) {
newErrors.phone = VALIDATION_MESSAGES.PHONE;
}
// Array fields
if (!data.items || data.items.length === 0) {
newErrors.items = 'At least one item is required';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
Use validators from @/lib/malawi-validators for phone, national ID, TIN, and business registration fields.
useImperativeHandle Pattern
Expose form submission to parent CrudPage:
// Create form: advances step or submits on final step
useImperativeHandle(ref, () => ({
handleSubmit: () => {
if (currentStep === STEPS.length) {
handleSubmit();
} else {
handleNext();
}
}
}));
// Edit form: saves the active tab
useImperativeHandle(ref, () => ({
handleSubmit: () => {
switch (activeTab) {
case 'section-a': return handleSaveSectionA();
case 'section-b': return handleSaveSectionB();
}
}
}));
Create Form Submission
Submit all entities sequentially, using the parent ID for children:
const handleSubmit = async () => {
if (isSubmitting) return; // Double-submit guard
setIsSubmitting(true);
try {
// 1. Create parent entity
const parentRes = await fetch('/api/entities', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify(snakefiyKeys(parentData)),
});
const parentJson = await parentRes.json();
const parentId = parentJson.data.id;
// 2. Create child entities using parent ID
await fetch('/api/child-entities', {
method: 'POST',
body: JSON.stringify({ ...snakefiyKeys(childData), parent_id: parentId }),
headers: { /* same headers */ },
});
// 3. Upsert for entities that may be auto-created by backend
await fetch('/api/auto-entities/upsert', {
method: 'POST',
body: JSON.stringify({ ...snakefiyKeys(autoData), parent_id: parentId }),
headers: { /* same headers */ },
});
// 4. Create array children (loop)
for (const member of members) {
await fetch('/api/members', {
method: 'POST',
body: JSON.stringify({ ...snakefiyKeys(member), parent_id: parentId }),
headers: { /* same headers */ },
});
}
onSuccess('Entity created successfully');
onEntityCreated?.({ id: parentId, ...parentJson.data });
} catch (error) {
onError?.(error);
} finally {
setIsSubmitting(false);
}
};
Edit Form: Data Loading Wrapper
The edit form uses a wrapper component to fetch related data in parallel:
export const EntityEditForm = forwardRef<any, Props>(({ item, ...props }, ref) => {
const [relatedA, setRelatedA] = useState<TypeA | undefined>();
const [relatedB, setRelatedB] = useState<TypeB | undefined>();
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchRelated = async () => {
setLoading(true);
const [aRes, bRes] = await Promise.all([
axios.get(`/api/entities/${item.id}/related_a`).catch(() => ({ data: { data: null } })),
axios.get(`/api/entities/${item.id}/related_b`).catch(() => ({ data: { data: [] } })),
]);
setRelatedA(convertSnakeToCamel(aRes.data.data));
setRelatedB(convertSnakeToCamel(bRes.data.data));
setLoading(false);
};
if (item?.id) fetchRelated();
}, [item?.id]);
if (loading) return <Loader2 className="h-8 w-8 animate-spin" />;
return <EntityEditFormSimple ref={ref} item={item} relatedA={relatedA} relatedB={relatedB} {...props} />;
});
Edit Form: Tab-Based Save
Each tab saves independently with its own validation and API call:
const handleSaveSection = async () => {
const newErrors: Record<string, string> = {};
// validate...
if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; }
setErrors({});
setIsSaving?.(true);
try {
const response = await fetch(`/api/sections/${section.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
body: JSON.stringify(snakefiyKeys(sectionData)),
});
if (response.ok) onSuccess('Section updated');
else onError?.(await response.json().catch(() => ({})));
} catch (error) { onError?.(error); }
finally { setIsSaving?.(false); }
};
Edit Form: Additional Members (Array Children)
Track original IDs to detect deletions:
const [members, setMembers] = useState<MemberData[]>(initialMembers);
const [originalMemberIds] = useState<number[]>(initialMembers.filter(m => m.id).map(m => m.id!));
const handleSaveMembers = async () => {
const currentIds = members.filter(m => m.id).map(m => m.id!);
const deletedIds = originalMemberIds.filter(id => !currentIds.includes(id));
// Delete removed
for (const id of deletedIds) {
await fetch(`/api/members/${id}`, { method: 'DELETE', headers: {...} });
}
// Create or update
for (const member of members) {
if (member.id) {
await fetch(`/api/members/${member.id}`, { method: 'PUT', body: JSON.stringify(snakefiyKeys({...member, parentId})), headers: {...} });
} else {
const res = await fetch('/api/members', { method: 'POST', body: JSON.stringify(snakefiyKeys({...member, parentId})), headers: {...} });
if (res.ok) { const data = await res.json(); member.id = data.data?.id; }
}
}
setMembers([...members]);
onSuccess('Members saved');
};
UI Components Used
| Purpose | Component | Import |
|---|---|---|
| Text input | Input | @/components/ui/input |
| Select dropdown | Select, SelectContent, SelectItem, SelectTrigger, SelectValue | `@/components/ui/select |
Content truncated.
When not to use it
- →When creating simple forms that do not span multiple sections
- →When forms do not require per-step validation
- →When forms do not involve related entities
Limitations
- →Creation forms use a sequential wizard with a Progress bar
- →Editing forms use a tab-based interface with Tabs
- →Per-step validation is required before advancing
How it compares
This approach provides architectural patterns and principles for complex forms, ensuring consistency and maintainability, unlike ad-hoc form development.
Compared to similar skills
multi-step-form side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| multi-step-form (this skill) | 0 | 5mo | Review | Intermediate |
| screenshot-to-code | 204 | 1mo | No flags | Beginner |
| web-artifacts-builder | 49 | 3mo | Review | Intermediate |
| radix-ui-design-system | 33 | 1mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
screenshot-to-code
OneWave-AI
Convert UI screenshots into working HTML/CSS/React/Vue code. Detects design patterns, components, and generates responsive layouts. Use this when users provide screenshots of websites, apps, or UI designs and want code implementation.
web-artifacts-builder
anthropics
Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
radix-ui-design-system
sickn33
Build accessible design systems with Radix UI primitives. Headless component customization, theming strategies, and compound component patterns for production-grade UI libraries.
figma-integration
duongdev
Guides design-to-code workflow using Figma integration. Helps extract designs, analyze components, and generate implementation specs. Auto-activates when users mention Figma URLs, design implementation, component conversion, or design-to-code workflows. Works with /ccpm:planning:design-ui, design-approve, design-refine, and /ccpm:utils:figma-refresh commands.
3d-web-experience
davila7
Expert in building 3D experiences for the web - Three.js, React Three Fiber, Spline, WebGL, and interactive 3D scenes. Covers product configurators, 3D portfolios, immersive websites, and bringing depth to web experiences. Use when: 3D website, three.js, WebGL, react three fiber, 3D experience.
tamagui
tamagui
Universal React UI framework for web and native. Use when building cross-platform apps with Tamagui, creating styled components with `styled()`, configuring design tokens/themes, using Tamagui UI components, or working with animations. Triggers: "tamagui", "styled()", "$token", "XStack/YStack", "useTheme", "@tamagui/*" imports, "createStyledContext", "variants".