Skip to content

Commit 95170f9

Browse files
fedecarboclaude
andcommitted
Add "Saving pattern" to the pattern library
Documents the BOPS saving convention: "Save and mark as complete" vs "Save changes" buttons, success confirmation banner, and return to task list. Four stories with MDX documentation following the same structure as the "Asking multiple things at once" pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e30b95d commit 95170f9

2 files changed

Lines changed: 390 additions & 0 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Saving pattern — how BOPS lets caseworkers save their work on a task.
3+
*
4+
* Two save actions ("Save and mark as complete" vs "Save changes"), a strong
5+
* success confirmation, and a return to the task list with the task now
6+
* showing as Completed. Markup mirrors the real BOPS task stories so the
7+
* pattern preview reads like the live application.
8+
*/
9+
import { mockData, renderStatusTag } from "../../helpers";
10+
11+
const { application } = mockData;
12+
13+
export default {
14+
title: "Patterns/Saving pattern",
15+
parameters: {
16+
layout: "padded",
17+
},
18+
decorators: [
19+
(story) =>
20+
`<div style="max-width: 1100px; margin: 0 auto; padding: 20px;">${story()}</div>`,
21+
],
22+
};
23+
24+
// ---------------------------------------------------------------------------
25+
// Wireframe shell — matches the Asking-multiple-things pattern so all pattern
26+
// previews share the same chrome.
27+
// ---------------------------------------------------------------------------
28+
29+
function renderShell(innerHtml, { caption } = {}) {
30+
return `
31+
<div class="sp-wireframe">
32+
<div class="sp-wireframe__header">
33+
<span class="sp-wireframe__logo">GOV.UK</span>
34+
<span class="sp-wireframe__service">Back Office Planning System</span>
35+
</div>
36+
<div class="sp-wireframe__bar">
37+
<span>${application.reference}</span>
38+
<span style="margin: 0 8px; opacity: 0.5;">|</span>
39+
<span>${application.address}</span>
40+
</div>
41+
<div class="sp-wireframe__body">
42+
${innerHtml}
43+
</div>
44+
${caption ? `<div class="sp-wireframe__caption">${caption}</div>` : ""}
45+
</div>
46+
47+
<style>
48+
.sp-wireframe {
49+
border: 1px solid #b1b4b6;
50+
border-radius: 6px;
51+
overflow: hidden;
52+
font-family: "GDS Transport", arial, sans-serif;
53+
background: #fff;
54+
}
55+
.sp-wireframe__header {
56+
background: #0b0c0c;
57+
padding: 10px 20px;
58+
display: flex;
59+
align-items: baseline;
60+
gap: 12px;
61+
}
62+
.sp-wireframe__logo {
63+
color: #fff;
64+
font-weight: bold;
65+
font-size: 18px;
66+
}
67+
.sp-wireframe__service {
68+
color: #fff;
69+
font-size: 16px;
70+
}
71+
.sp-wireframe__bar {
72+
background: #f3f2f1;
73+
padding: 8px 20px;
74+
font-size: 14px;
75+
color: #0b0c0c;
76+
border-bottom: 1px solid #b1b4b6;
77+
}
78+
.sp-wireframe__body {
79+
padding: 30px 40px 40px;
80+
}
81+
.sp-wireframe__caption {
82+
background: #f3f2f1;
83+
border-top: 1px solid #b1b4b6;
84+
padding: 10px 20px;
85+
font-size: 13px;
86+
color: #505a5f;
87+
}
88+
</style>`;
89+
}
90+
91+
// ---------------------------------------------------------------------------
92+
// A compact example of a real BOPS task form — the "Confirm site notice is in
93+
// place" task. Used as the body above the save buttons so the button variants
94+
// sit in a realistic context instead of floating on their own.
95+
// ---------------------------------------------------------------------------
96+
97+
function renderTaskFormBody() {
98+
return `
99+
<h1 class="govuk-heading-l">Confirm site notice is in place</h1>
100+
101+
<div class="govuk-form-group">
102+
<fieldset class="govuk-fieldset" role="group">
103+
<legend class="govuk-fieldset__legend govuk-fieldset__legend--s">
104+
What date was the site notice displayed?
105+
</legend>
106+
<div class="govuk-date-input">
107+
<div class="govuk-date-input__item">
108+
<div class="govuk-form-group">
109+
<label class="govuk-label govuk-date-input__label" for="displayed-day">Day</label>
110+
<input class="govuk-input govuk-date-input__input govuk-input--width-2" id="displayed-day" name="displayed[day]" type="text" inputmode="numeric" value="12">
111+
</div>
112+
</div>
113+
<div class="govuk-date-input__item">
114+
<div class="govuk-form-group">
115+
<label class="govuk-label govuk-date-input__label" for="displayed-month">Month</label>
116+
<input class="govuk-input govuk-date-input__input govuk-input--width-2" id="displayed-month" name="displayed[month]" type="text" inputmode="numeric" value="04">
117+
</div>
118+
</div>
119+
<div class="govuk-date-input__item">
120+
<div class="govuk-form-group">
121+
<label class="govuk-label govuk-date-input__label" for="displayed-year">Year</label>
122+
<input class="govuk-input govuk-date-input__input govuk-input--width-4" id="displayed-year" name="displayed[year]" type="text" inputmode="numeric" value="2026">
123+
</div>
124+
</div>
125+
</div>
126+
</fieldset>
127+
</div>
128+
129+
<div class="govuk-form-group">
130+
<label class="govuk-label govuk-label--s" for="evidence-upload">
131+
Upload evidence of site notice in place
132+
</label>
133+
<div class="govuk-hint">Add any photos of the site notice being displayed.</div>
134+
<input class="govuk-file-upload" id="evidence-upload" name="documents[]" type="file" multiple>
135+
</div>`;
136+
}
137+
138+
// ---------------------------------------------------------------------------
139+
// Story 1 — Save and mark as complete (primary action)
140+
// ---------------------------------------------------------------------------
141+
142+
export const SaveAndMarkAsComplete = {
143+
name: "Save and mark as complete (primary)",
144+
render: () =>
145+
renderShell(
146+
`
147+
<div class="govuk-grid-row">
148+
<div class="govuk-grid-column-two-thirds">
149+
${renderTaskFormBody()}
150+
151+
<div class="govuk-button-group govuk-!-padding-top-4">
152+
<button type="submit" class="govuk-button" data-module="govuk-button">
153+
Save and mark as complete
154+
</button>
155+
<a class="govuk-link" href="#">Back</a>
156+
</div>
157+
</div>
158+
</div>
159+
`,
160+
{
161+
caption:
162+
"The primary save action on a task form. 'Save and mark as complete' persists the user's work AND changes the task's status to Completed on the task list. Use this when the user is declaring the task done.",
163+
}
164+
),
165+
};
166+
167+
// ---------------------------------------------------------------------------
168+
// Story 2 — Save changes only (secondary action)
169+
// ---------------------------------------------------------------------------
170+
171+
export const SaveChangesOnly = {
172+
name: "Save changes (secondary)",
173+
render: () =>
174+
renderShell(
175+
`
176+
<div class="govuk-grid-row">
177+
<div class="govuk-grid-column-two-thirds">
178+
${renderTaskFormBody()}
179+
180+
<div class="govuk-button-group govuk-!-padding-top-4">
181+
<button type="submit" class="govuk-button govuk-button--secondary" data-module="govuk-button">
182+
Save changes
183+
</button>
184+
<a class="govuk-link" href="#">Back</a>
185+
</div>
186+
</div>
187+
</div>
188+
`,
189+
{
190+
caption:
191+
"The secondary save action. 'Save changes' persists the user's work without marking the task complete, so they can come back to it later. Use this for in-progress work or when a caseworker is interrupted.",
192+
}
193+
),
194+
};
195+
196+
// ---------------------------------------------------------------------------
197+
// Story 3 — Success confirmation banner
198+
// ---------------------------------------------------------------------------
199+
200+
export const SuccessBanner = {
201+
name: "Success confirmation",
202+
render: () =>
203+
renderShell(
204+
`
205+
<div class="govuk-grid-row">
206+
<div class="govuk-grid-column-two-thirds">
207+
<div class="govuk-notification-banner govuk-notification-banner--success" role="alert"
208+
aria-labelledby="govuk-notification-banner-title"
209+
data-module="govuk-notification-banner">
210+
<div class="govuk-notification-banner__header">
211+
<h2 class="govuk-notification-banner__title" id="govuk-notification-banner-title">
212+
Success
213+
</h2>
214+
</div>
215+
<div class="govuk-notification-banner__content">
216+
<h3 class="govuk-notification-banner__heading">
217+
Site notice confirmation has been saved and the task has been marked as complete.
218+
</h3>
219+
</div>
220+
</div>
221+
222+
<h1 class="govuk-heading-l">Consultation</h1>
223+
<p class="govuk-body">Return to the task list to continue the next task.</p>
224+
<a class="govuk-link" href="#">Back to application overview</a>
225+
</div>
226+
</div>
227+
`,
228+
{
229+
caption:
230+
"The success banner names exactly what was saved and what state changed. Specific wording beats a generic 'Saved successfully' — it gives caseworkers confidence their work landed and reminds them what they just did.",
231+
}
232+
),
233+
};
234+
235+
// ---------------------------------------------------------------------------
236+
// Story 4 — Return to the task list with the task now Completed
237+
// ---------------------------------------------------------------------------
238+
239+
function renderConsultationTaskList() {
240+
const tasks = [
241+
{ name: "Select consultees", status: "complete" },
242+
{ name: "Send site notice", status: "complete" },
243+
{ name: "Confirm site notice is in place", status: "complete" },
244+
{ name: "Send letters to neighbours", status: "in_progress" },
245+
{ name: "Create press notice", status: "not_started" },
246+
{ name: "View consultee responses", status: "not_started" },
247+
];
248+
249+
const items = tasks
250+
.map(
251+
(task) => `
252+
<li class="app-task-list__item" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #b1b4b6;">
253+
<span class="app-task-list__task-name"><a class="govuk-link" href="#">${task.name}</a></span>
254+
<span class="app-task-list__task-tag">${renderStatusTag(task.status)}</span>
255+
</li>`
256+
)
257+
.join("");
258+
259+
return `
260+
<h2 class="govuk-heading-m">Consultation</h2>
261+
<ul class="app-task-list__items" style="list-style: none; padding: 0; margin: 0;">
262+
${items}
263+
</ul>`;
264+
}
265+
266+
export const ReturnToTaskList = {
267+
name: "Return to task list (what's next)",
268+
render: () =>
269+
renderShell(
270+
`
271+
<div class="govuk-grid-row">
272+
<div class="govuk-grid-column-two-thirds">
273+
<span class="govuk-caption-l">${application.reference}</span>
274+
<h1 class="govuk-heading-l">Application</h1>
275+
276+
${renderConsultationTaskList()}
277+
</div>
278+
</div>
279+
`,
280+
{
281+
caption:
282+
"After saving, the user lands back on the task list. The task they just completed now shows a 'Completed' tag, and the next task they could pick up is still visible in context — so the save action flows naturally into the next piece of work.",
283+
}
284+
),
285+
};
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Meta } from "@storybook/addon-docs/blocks";
2+
import { GdsCanvas } from "@sb-components/GdsCanvas";
3+
import * as SavingStories from "./SavingPattern.stories";
4+
5+
<Meta title="Patterns/Saving pattern/Documentation" />
6+
7+
# Saving pattern
8+
9+
A pattern for back-office services where a task within a larger task list needs two distinct save actions — one to persist work in progress, and one to declare the task complete — paired with a specific success message and a clear return to the task list.
10+
11+
Caseworkers rarely complete a task in a single sitting. They get interrupted, they wait for evidence, they hand work over. The saving pattern is how the interface stays trustworthy through all of that: the user always knows the difference between "I'm coming back to this" and "I'm done", and the system always tells them exactly what just happened.
12+
13+
<GdsCanvas of={SavingStories.SaveAndMarkAsComplete} />
14+
15+
<hr />
16+
17+
## When to use this pattern
18+
19+
Use this pattern when:
20+
21+
- the screen is a **task within a larger task list** — completion is a meaningful state change visible elsewhere in the service
22+
- users are **expert caseworkers** who work across many tasks and many applications in parallel
23+
- tasks are **often interrupted** — waiting on documents, colleagues, applicants, or evidence
24+
- the task has a **binary complete / not-complete state** that other people in the service rely on (a reviewer, an auditor, a next-stage workflow)
25+
- users need **confidence that their work has been saved** before they move on, not just an assumption
26+
27+
## When not to use this pattern
28+
29+
Do not use this pattern when:
30+
31+
- the form is **a single inline edit** with no meaningful "complete" state — a plain Save button is enough
32+
- the form **autosaves** — don't ask the user to press a button for something the system is already doing
33+
- the screen is **not part of a task list** — "mark as complete" has nothing to mark against
34+
- the action is **destructive or irreversible** (sending a notice, issuing a decision) — use a dedicated action verb like "Send" or "Issue", not "Save"
35+
36+
<hr />
37+
38+
## How it works
39+
40+
### Two save actions, not one
41+
42+
Every task form offers two distinct actions:
43+
44+
- **Save and mark as complete** (primary) — persists the user's work *and* flips the task's status on the task list to Completed. This is how the user declares "I'm done with this task."
45+
- **Save changes** (secondary) — persists the user's work without changing the task's status. This is how the user says "I'm still working on this, but I want my progress safe."
46+
47+
Splitting these into two buttons removes a judgement the user would otherwise have to make every single time they save. Bundling save and completion into one button leaves in-progress work stranded; offering only "Save" hides the completion step somewhere else and forces the user to remember it.
48+
49+
<GdsCanvas of={SavingStories.SaveChangesOnly} />
50+
51+
### Give a specific success confirmation
52+
53+
After a successful save, show a green notification banner that **names exactly what was saved and what state changed**. "Site notice confirmation has been saved and the task has been marked as complete" is stronger than "Saved successfully" — it reassures the caseworker that the right thing happened, and it reminds them of the decision they just made, which matters when they're moving between many cases.
54+
55+
<GdsCanvas of={SavingStories.SuccessBanner} />
56+
57+
### Return the user to the task list
58+
59+
After saving, the user lands back on the task list, with the task they just completed now showing a "Completed" tag. The next task they could pick up is still visible in context. This turns the save action into a natural handoff into the next piece of work rather than a dead end that makes the caseworker hunt for where to go next.
60+
61+
<GdsCanvas of={SavingStories.ReturnToTaskList} />
62+
63+
### Keep the wording consistent across every task
64+
65+
The button labels and success messages should read the same way across every task in the service. Inconsistency here is expensive: if one task says "Save and mark as complete" and another says "Finish task" and a third says "Done", the user has to re-learn the pattern every time. Pick one wording and hold it.
66+
67+
### Write success messages in the active voice
68+
69+
Name the thing that was saved, in sentence case, in the active voice, the way GDS [content guidelines](https://www.gov.uk/guidance/content-design/writing-for-gov-uk) recommend. Avoid passive phrasing ("Your changes have been saved") — it's longer and weaker than naming the thing directly.
70+
71+
<hr />
72+
73+
## Research and learnings
74+
75+
This pattern comes from observing how caseworkers actually move through a long task list over the course of a day — rarely linearly, rarely in one sitting, and almost always with interruptions between tasks.
76+
77+
### What we've learned
78+
79+
- **Caseworkers need to distinguish "saving progress" from "declaring done".** Bundling those into one button forces a judgement on every save and loses in-progress work when the user isn't ready to commit.
80+
81+
- **Confidence matters more than speed at the save step.** Users will tolerate a full-page reload and a prominent banner if it tells them clearly that their work has landed. A silent save feels like it might not have worked.
82+
83+
- **Specific success messages outperform generic ones.** Naming the thing that was saved ("Site notice confirmation has been saved...") gives the user a moment to verify in their own head that the right action happened, which reduces the number of times they go back in to check.
84+
85+
- **The task list is the anchor.** Returning to the task list after save is what makes the whole pattern hang together — the completed tag is the visible proof that the save worked, and the remaining tasks are the prompt for what to do next.
86+
87+
- **Consistency across the whole service compounds.** Every task that uses the same two-button pattern and the same success-banner shape makes every other task easier to learn.
88+
89+
<div style={{ marginTop: "16px", padding: "16px 20px", background: "#f3f2f1", borderLeft: "4px solid #1d70b8" }}>
90+
91+
**Example: how we apply this in BOPS**
92+
93+
The Back Office Planning System (BOPS) presents a planning application as a task list across five workflow stages (Validation, Consultation, Assessment, Review, Determination). A single application can involve 20+ tasks and multiple actors (case officer, consultee, reviewer), often spread over weeks.
94+
95+
Most task forms in BOPS use the two-button pattern described here. For example, the **Confirm site notice is in place** task (shown above) lets officers upload evidence and mark the task complete, while **Check description** lets officers save a change request mid-flow without declaring the task done. After saving, officers see a specific success banner naming what was saved, and land back on the Consultation task list with the task now tagged Completed — ready to pick up the next task in the stage.
96+
97+
See live examples in [Confirm Site Notice](?path=/docs/workflows-planning-permission-2-consultation-site-notice--confirm-notice) and [Send Letters to Neighbours](?path=/docs/workflows-planning-permission-2-consultation-send-letters-to-neighbours--success).
98+
99+
</div>
100+
101+
<div style={{ marginTop: "16px", padding: "16px 20px", background: "#f3f2f1", borderLeft: "4px solid #505a5f" }}>
102+
103+
**This is an evolving pattern.** The two-button split works well for tasks with a clear complete/not-complete state, but there are edge cases we're still thinking about — tasks where "complete" depends on an external actor, tasks that need to branch into a follow-up rather than returning to the task list, and tasks that should probably autosave instead of using this pattern at all. If you're considering this pattern for a different back-office service, start with the task list as your anchor and work outwards from there.
104+
105+
</div>

0 commit comments

Comments
 (0)