Creating multi-page forms

Learn how to build multi-step forms with validation, conditional logic, webhook integration and summary pages using the GOV.UK Design System.

This guidance follows GOV.UK Design System patterns and provides practical examples for developers building government services.

Contents


Overview

Multi-page forms break complex processes into manageable steps, improving user experience and reducing cognitive load. They're essential for government services that collect detailed information.

When to use multi-page forms

  • When collecting more than 10 fields of information
  • When different sections have different purposes
  • When you need conditional branching based on answers
  • When users might need to return to complete the form later

Key principles

Important Always follow the 'one thing per page' principle - each page should have a single purpose or question.

Form structure

Basic page flow

A typical multi-page form follows this structure:

  1. Start page - Explains what users need and how long it takes
  2. Question pages - Individual steps collecting specific information
  3. Check your answers - Summary page for review
  4. Declaration - Legal statements and submission
  5. Confirmation - Success page with reference number

URL structure

Use clear, descriptive URLs for each step:

/apply-for-licence/start
/apply-for-licence/personal-details
/apply-for-licence/business-details
/apply-for-licence/check-your-answers
/apply-for-licence/declaration
/apply-for-licence/confirmation

Session management

Store form data in server-side sessions to allow users to:

  • Navigate between pages without losing data
  • Return to the form later (if appropriate)
  • Use the browser's back button safely
Example: Express.js session setup
const session = require('express-session');

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { 
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

Validation patterns

Server-side validation

Always validate on the server - never rely solely on client-side validation for security.

Error handling

When validation fails:

  1. Show an error summary at the top of the page
  2. Add error styling to the affected form controls
  3. Include specific error messages next to each field
  4. Set focus to the error summary
Example: Validation middleware
function validatePersonalDetails(req, res, next) {
  const errors = [];
  const { firstName, lastName, email } = req.body;
  
  if (!firstName || firstName.trim().length < 2) {
    errors.push({
      field: 'firstName',
      message: 'Enter your first name'
    });
  }
  
  if (!email || !email.includes('@')) {
    errors.push({
      field: 'email', 
      message: 'Enter a valid email address'
    });
  }
  
  if (errors.length > 0) {
    req.session.errors = errors;
    return res.redirect('/personal-details');
  }
  
  // Save valid data to session
  req.session.personalDetails = { firstName, lastName, email };
  next();
}

Field validation rules

Common validation patterns
Field type Validation rule Error message
Required text Field is not empty Enter your [field name]
Email Contains @ and valid format Enter an email address in the correct format
Date Valid date in DD/MM/YYYY Enter a real date
Postcode UK postcode format Enter a full UK postcode

Conditional logic

Conditional questions

Show or hide questions based on previous answers to create personalized user journeys.

Implementation patterns

1. Conditional reveal (same page)

Use for simple follow-up questions:

Example: Conditional reveal HTML
<div class="govuk-radios__conditional govuk-radios__conditional--hidden" id="conditional-contact-email">
  <div class="govuk-form-group">
    <label class="govuk-label" for="email">
      Email address
    </label>
    <input class="govuk-input" id="email" name="email" type="email">
  </div>
</div>

2. Conditional routing (different pages)

Route users to different pages based on their answers:

Example: Conditional routing logic
app.post('/business-type', (req, res) => {
  const businessType = req.body.businessType;
  req.session.businessType = businessType;
  
  if (businessType === 'limited-company') {
    res.redirect('/company-details');
  } else if (businessType === 'sole-trader') {
    res.redirect('/personal-details');
  } else {
    res.redirect('/partnership-details');
  }
});

Managing complex logic

For complex conditional logic, consider using a routing function:

Example: Routing function
function getNextPage(currentPage, sessionData) {
  const routes = {
    'start': 'eligibility',
    'eligibility': (data) => {
      return data.eligible === 'yes' ? 'personal-details' : 'not-eligible';
    },
    'personal-details': (data) => {
      return data.businessType === 'company' ? 'company-details' : 'business-details';
    },
    'company-details': 'check-answers',
    'business-details': 'check-answers'
  };
  
  const nextRoute = routes[currentPage];
  
  if (typeof nextRoute === 'function') {
    return nextRoute(sessionData);
  }
  
  return nextRoute;
}

Webhook integration

Secure webhook submission

Submit form data to external systems (like n8n) without exposing webhook URLs to users.

Data preparation

Before sending to webhook, prepare a clean data object:

Example: Webhook submission function
async function submitToWebhook(sessionData, req) {
  const submissionData = {
    // Form data
    personalDetails: sessionData.personalDetails,
    businessDetails: sessionData.businessDetails,
    
    // Metadata
    submissionId: generateUniqueId(),
    timestamp: new Date().toISOString(),
    userAgent: req.get('User-Agent'),
    ipAddress: req.ip,
    
    // Form tracking
    formVersion: '1.2',
    formType: 'business-registration',
    source: 'gov-website'
  };
  
  try {
    const response = await axios.post(process.env.WEBHOOK_URL, submissionData, {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.WEBHOOK_TOKEN}`
      },
      timeout: 10000
    });
    
    return response.data;
  } catch (error) {
    console.error('Webhook submission failed:', error);
    throw new Error('Failed to submit form');
  }
}

Error handling

Always handle webhook failures gracefully:

  • Store submission data locally as backup
  • Show appropriate error messages to users
  • Implement retry logic for temporary failures
  • Log errors for monitoring

Summary pages

Check your answers pattern

The 'Check your answers' page lets users review their information before submitting.

Summary list component

Use the GOV.UK summary list to display collected information:

Example: Summary list HTML
<dl class="govuk-summary-list">
  <div class="govuk-summary-list__row">
    <dt class="govuk-summary-list__key">
      Name
    </dt>
    <dd class="govuk-summary-list__value">
       
    </dd>
    <dd class="govuk-summary-list__actions">
      <a class="govuk-link" href="/personal-details">
        Change<span class="govuk-visually-hidden"> name</span>
      </a>
    </dd>
  </div>
</dl>

Change links

Each summary row should include a 'Change' link that returns users to the relevant page while preserving their data.

Complete code examples

Express.js route structure

Example: Complete route implementation
// GET route - show form page
app.get('/personal-details', (req, res) => {
  res.render('personal-details', {
    data: req.session.personalDetails || {},
    errors: req.session.errors || []
  });
  
  // Clear errors after displaying
  delete req.session.errors;
});

// POST route - process form submission
app.post('/personal-details', 
  validatePersonalDetails,
  (req, res) => {
    // Save to session
    req.session.personalDetails = req.body;
    
    // Determine next page
    const nextPage = getNextPage('personal-details', req.session);
    res.redirect(`/${nextPage}`);
  }
);

Template structure

Example: Form template with validation


<form method="post">
  <div class="govuk-form-group ">
    <label class="govuk-label" for="firstName">First name</label>
    
    <input class="govuk-input" id="firstName" name="firstName" value="">
  </div>
  
  <button class="govuk-button">Continue</button>
</form>

Remember: This guidance provides patterns and examples. Always test your forms with real users and consider accessibility requirements for your specific service.