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.
Contents
- Overview
- Form structure
- Validation patterns
- Conditional logic
- Webhook integration
- Summary pages
- Code examples
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
Form structure
Basic page flow
A typical multi-page form follows this structure:
- Start page - Explains what users need and how long it takes
- Question pages - Individual steps collecting specific information
- Check your answers - Summary page for review
- Declaration - Legal statements and submission
- 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:
- Show an error summary at the top of the page
- Add error styling to the affected form controls
- Include specific error messages next to each field
- 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
| Field type | Validation rule | Error message |
|---|---|---|
| Required text | Field is not empty | Enter your [field name] |
| 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.