MU

multi-step-form

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

Installs 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.
281 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Intermediate

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

You give it
Form requirements for complex data collection
You get back
A multi-step form implementation with sequential wizard or tab-based editing

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:

ModePatternNavigationSave Behavior
CreateSequential wizard with Progress barNext/Previous buttons, per-step validation gatesSingle final submission creates all entities
EditTab-based interface with TabsClick any tab freelyEach tab saves independently

Key Principles

  1. Create = wizard, Edit = tabs - Creation walks users through steps in order. Editing lets users jump to any section.
  2. Per-step validation before advancing - Never let the user proceed to the next step with invalid data.
  3. Single error state - Use one errors: Record<string, string> for the entire form, cleared on step change or successful validation.
  4. forwardRef + useImperativeHandle - Expose handleSubmit to the parent CrudPage component. On create forms, this either advances to next step or submits on the final step.
  5. Sequential API calls on create - Create the parent entity first, then child entities using the parent's returned ID.
  6. Independent saves on edit - Each tab has its own save handler. Only the active tab saves when the user clicks Save.
  7. Double submission guard - Use an isSubmitting flag to prevent duplicate submissions.
  8. 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

PurposeComponentImport
Text inputInput@/components/ui/input
Select dropdownSelect, 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.

SkillInstallsUpdatedSafetyDifficulty
multi-step-form (this skill)05moReviewIntermediate
screenshot-to-code2041moNo flagsBeginner
web-artifacts-builder493moReviewIntermediate
radix-ui-design-system331moReviewIntermediate

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.

204389

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.

49162

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.

33130

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.

23129

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.

3570

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

2478

Search skills

Search the agent skills registry