Skip to content

Commit 2408e66

Browse files
fedecarboclaude
andcommitted
Add Application Header Bar pattern story and documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c1d0587 commit 2408e66

2 files changed

Lines changed: 326 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Application Header Bar — a persistent context bar shown below the GOV.UK
3+
* header on every planning application page, giving case officers instant
4+
* access to the key application facts without taking up task-form space.
5+
*/
6+
import { mockData } from "../../helpers";
7+
8+
export default {
9+
title: "Patterns/Application Header Bar",
10+
parameters: {
11+
layout: "padded",
12+
},
13+
decorators: [
14+
(story) =>
15+
`<div style="max-width: 1100px; margin: 0 auto; padding: 20px;">${story()}</div>`,
16+
],
17+
};
18+
19+
// ---------------------------------------------------------------------------
20+
// Helpers
21+
// ---------------------------------------------------------------------------
22+
23+
function renderHeaderBar({
24+
reference = mockData.application.reference,
25+
address = mockData.application.address.full,
26+
description = mockData.application.description,
27+
showApplicationInfoLink = true,
28+
descriptionExpanded = false,
29+
} = {}) {
30+
const togglePanelClass = descriptionExpanded
31+
? "bops-header-bar__toggle-panel"
32+
: "bops-header-bar__toggle-panel govuk-!-display-none";
33+
34+
const toggleButtonText = descriptionExpanded
35+
? "Hide proposal description"
36+
: "Show proposal description";
37+
38+
const ariaExpanded = descriptionExpanded ? "true" : "false";
39+
40+
const rightItems = showApplicationInfoLink
41+
? `<div class="bops-header-bar__list--right">
42+
<li class="bops-header-bar__item">
43+
<a href="#" class="govuk-link govuk-link--no-visited-state" target="_blank" rel="noopener noreferrer">Application information</a>
44+
</li>
45+
</div>`
46+
: `<div class="bops-header-bar__list--right"></div>`;
47+
48+
return `
49+
<div class="bops-header-bar"
50+
data-controller="toggle"
51+
data-toggle-class-name-value="govuk-!-display-none"
52+
data-toggle-condensed-text-value="Show proposal description"
53+
data-toggle-expanded-text-value="Hide proposal description">
54+
<ul class="bops-header-bar__list">
55+
<li class="bops-header-bar__item">
56+
<strong><a href="#" class="govuk-link govuk-link--no-visited-state">${reference}</a></strong>
57+
</li>
58+
<li class="bops-header-bar__item">${address}</li>
59+
<li class="bops-header-bar__item--toggle">
60+
<button
61+
type="button"
62+
class="button-as-link govuk-link govuk-link--no-visited-state"
63+
data-toggle-target="button"
64+
data-action="toggle#click"
65+
aria-expanded="${ariaExpanded}">
66+
${toggleButtonText}
67+
</button>
68+
</li>
69+
${rightItems}
70+
</ul>
71+
<div class="${togglePanelClass}" data-toggle-target="content" aria-live="polite">
72+
<p class="govuk-!-margin-0">${description}</p>
73+
</div>
74+
</div>`;
75+
}
76+
77+
function renderInLayout(headerBarHtml) {
78+
return `
79+
<div class="pattern-wireframe">
80+
<div class="pattern-wireframe__header">
81+
<span class="pattern-wireframe__logo">GOV.UK</span>
82+
<span class="pattern-wireframe__service">Back Office Planning System</span>
83+
</div>
84+
${headerBarHtml}
85+
<div class="pattern-wireframe__content" style="padding: 30px; background: #fff; min-height: 300px;">
86+
<h1 class="govuk-heading-l">Check description</h1>
87+
<div style="margin-top: 20px;">
88+
<div class="pattern-wireframe__placeholder-line" style="width: 90%; height: 12px; background: #dee0e2; border-radius: 3px; margin-bottom: 10px;"></div>
89+
<div class="pattern-wireframe__placeholder-line" style="width: 70%; height: 12px; background: #dee0e2; border-radius: 3px; margin-bottom: 10px;"></div>
90+
<div class="pattern-wireframe__placeholder-line" style="width: 55%; height: 12px; background: #dee0e2; border-radius: 3px; margin-bottom: 24px;"></div>
91+
<div style="height: 80px; background: #f3f2f1; border: 1px solid #dee0e2; border-radius: 4px; margin-bottom: 20px;"></div>
92+
<div style="width: 200px; height: 40px; background: #00703c; border-radius: 4px;"></div>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<style>
98+
.pattern-wireframe {
99+
border: 1px solid #b1b4b6;
100+
border-radius: 6px;
101+
overflow: hidden;
102+
font-family: "GDS Transport", arial, sans-serif;
103+
}
104+
.pattern-wireframe__header {
105+
background: #0b0c0c;
106+
padding: 10px 20px;
107+
display: flex;
108+
align-items: baseline;
109+
gap: 12px;
110+
}
111+
.pattern-wireframe__logo {
112+
color: #fff;
113+
font-weight: bold;
114+
font-size: 18px;
115+
}
116+
.pattern-wireframe__service {
117+
color: #fff;
118+
font-size: 16px;
119+
}
120+
</style>`;
121+
}
122+
123+
// ---------------------------------------------------------------------------
124+
// Stories
125+
// ---------------------------------------------------------------------------
126+
127+
export const Default = {
128+
name: "Default",
129+
render: () => renderHeaderBar(),
130+
};
131+
132+
export const DescriptionExpanded = {
133+
name: "Description expanded",
134+
render: () => renderHeaderBar({ descriptionExpanded: true }),
135+
};
136+
137+
export const WithoutApplicationInfoLink = {
138+
name: "Without application info link",
139+
render: () => renderHeaderBar({ showApplicationInfoLink: false }),
140+
};
141+
142+
export const InContext = {
143+
name: "In context",
144+
render: () => renderInLayout(renderHeaderBar()),
145+
};
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { Meta, Canvas } from "@storybook/blocks";
2+
import * as HeaderBarStories from "./ApplicationHeaderBar.stories";
3+
4+
<Meta title="Patterns/Application Header Bar/Documentation" />
5+
6+
# Application header bar
7+
8+
A persistent context bar shown below the GOV.UK header on every planning application page. It gives case officers instant access to the key application facts — reference, address, and proposal description — without that information taking up space inside the task form.
9+
10+
<Canvas of={HeaderBarStories.Default} />
11+
12+
<hr />
13+
14+
## Why this pattern exists
15+
16+
BOPS originally showed the full application details — reference, address, description, and more — at the top of every individual task page. This created two problems:
17+
18+
1. **Officers had to scroll past the details to reach the task.** On every page, they saw the same block of information before they could do anything useful.
19+
20+
2. **The details were duplicated everywhere.** Each task page maintained its own copy of the same information, making it harder to maintain consistency.
21+
22+
The team extracted this information into a single, dedicated bar that sits persistently above the task content. Officers always know which application they're working on without losing any task space.
23+
24+
<hr />
25+
26+
## When to use this pattern
27+
28+
Use the application header bar when:
29+
30+
- you are displaying a **task or form that belongs to a specific planning application**
31+
- officers will be moving between multiple tasks within the same application and need **persistent context** about which case they're on
32+
- the page layout already has a GOV.UK header at the top and you need an **application-specific second tier** below it
33+
34+
## When not to use this pattern
35+
36+
Do not use the application header bar when:
37+
38+
- the page is **not associated with a specific application** (for example, a dashboard, search results, or admin pages)
39+
- you are showing a **public-facing** page — this pattern is designed for back-office case officers, not applicants
40+
- the application context is already prominent elsewhere on the page (for example, a full application detail page)
41+
42+
<hr />
43+
44+
## How it works
45+
46+
### Three zones
47+
48+
The bar is divided into three horizontal zones:
49+
50+
1. **Reference and address (left)** — the application reference number, shown in bold and linked to the application overview, followed by the full site address. These are always visible and cannot be hidden.
51+
52+
2. **Description toggle (centre)** — a "Show proposal description" button that expands a panel below the bar revealing the full proposal description. This lets officers check the description text at any point without navigating away. The button toggles between "Show proposal description" and "Hide proposal description".
53+
54+
3. **Application information link (right)** — a link to the full Application Information page, which opens in a new tab. This link can be hidden in specific contexts where it would be redundant or inappropriate (for example, when the officer is already on the application information page).
55+
56+
### Description collapsed (default)
57+
58+
This is the state officers see on first load. The proposal description is hidden; the bar shows only the reference, address, toggle button, and link.
59+
60+
<Canvas of={HeaderBarStories.Default} />
61+
62+
### Description expanded
63+
64+
When an officer clicks "Show proposal description", a panel drops below the bar with the full proposal text. This is rendered as a separate story since the Stimulus.js toggle controller is not active in Storybook.
65+
66+
<Canvas of={HeaderBarStories.DescriptionExpanded} />
67+
68+
### Without application information link
69+
70+
In some contexts the application information link is suppressed — for example, when the officer is already viewing the application information page. The right zone is empty and the bar remains functional.
71+
72+
<Canvas of={HeaderBarStories.WithoutApplicationInfoLink} />
73+
74+
### In context
75+
76+
The bar sits directly below the black GOV.UK header, forming a two-tier header stack above the task content. Both tiers are sticky — they remain pinned at the top of the viewport as the officer scrolls through long task forms.
77+
78+
<Canvas of={HeaderBarStories.InContext} />
79+
80+
### Sticky behaviour
81+
82+
The header bar uses `.bops-headers--sticky` on its wrapper element, which applies `position: sticky; top: 0; z-index: 2000`. This keeps both the GOV.UK header and the application bar visible at all times. The sticky behaviour is disabled when the sidebar navigation pattern is active (they don't coexist).
83+
84+
### Responsive behaviour
85+
86+
On screens narrower than 768px, the bar stacks vertically — each item appears on its own line. The vertical dividers between items are hidden on mobile. The primary use case is desktop.
87+
88+
<hr />
89+
90+
## Implementation
91+
92+
### HTML structure
93+
94+
```html
95+
<div class="bops-header-bar"
96+
data-controller="toggle"
97+
data-toggle-class-name-value="govuk-!-display-none"
98+
data-toggle-condensed-text-value="Show proposal description"
99+
data-toggle-expanded-text-value="Hide proposal description">
100+
101+
<ul class="bops-header-bar__list">
102+
<!-- Left: reference and address -->
103+
<li class="bops-header-bar__item">
104+
<strong><a href="/planning_applications/123" class="govuk-link govuk-link--no-visited-state">BPS-24-00345-HAPP</a></strong>
105+
</li>
106+
<li class="bops-header-bar__item">12 Elm Grove, London, SE15 5DE</li>
107+
108+
<!-- Centre: description toggle button -->
109+
<li class="bops-header-bar__item--toggle">
110+
<button
111+
type="button"
112+
class="button-as-link govuk-link govuk-link--no-visited-state"
113+
data-toggle-target="button"
114+
data-action="toggle#click"
115+
aria-expanded="false">
116+
Show proposal description
117+
</button>
118+
</li>
119+
120+
<!-- Right: application information link -->
121+
<div class="bops-header-bar__list--right">
122+
<li class="bops-header-bar__item">
123+
<a href="/planning_applications/123/information" class="govuk-link govuk-link--no-visited-state" target="_blank" rel="noopener noreferrer">
124+
Application information
125+
</a>
126+
</li>
127+
</div>
128+
</ul>
129+
130+
<!-- Description panel (hidden by default) -->
131+
<div class="bops-header-bar__toggle-panel govuk-!-display-none"
132+
data-toggle-target="content"
133+
aria-live="polite">
134+
<p class="govuk-!-margin-0">Proposed two-storey rear extension...</p>
135+
</div>
136+
</div>
137+
```
138+
139+
### CSS classes
140+
141+
| Class | Purpose |
142+
|-------|---------|
143+
| `.bops-header-bar` | Bar wrapper — light blue background, bottom border, flex layout |
144+
| `.bops-header-bar__list` | Flex row containing all left items and the toggle button |
145+
| `.bops-header-bar__item` | Individual item — inline flex, padded, with a left border separator |
146+
| `.bops-header-bar__item--toggle` | Wrapper for the description toggle button |
147+
| `.bops-header-bar__list--right` | Right-aligned group for the application information link |
148+
| `.bops-header-bar__toggle-panel` | Collapsible description panel below the bar |
149+
| `.bops-headers--sticky` | Applied to the outer wrapper to pin both headers at the top of the viewport |
150+
151+
### Stimulus controller
152+
153+
The description toggle is powered by the `toggle` Stimulus controller. The controller reads three data attributes from the container element:
154+
155+
| Attribute | Value | Purpose |
156+
|-----------|-------|---------|
157+
| `data-controller` | `"toggle"` | Activates the controller on this element |
158+
| `data-toggle-class-name-value` | `"govuk-!-display-none"` | CSS class added/removed to show/hide the panel |
159+
| `data-toggle-condensed-text-value` | `"Show proposal description"` | Button label when panel is hidden |
160+
| `data-toggle-expanded-text-value` | `"Hide proposal description"` | Button label when panel is visible |
161+
162+
The button uses `data-toggle-target="button"` and the panel uses `data-toggle-target="content"`.
163+
164+
### Ruby (ViewComponent)
165+
166+
```ruby
167+
render BopsCore::HeaderBarComponent.new(
168+
left: [
169+
{ text: tag.strong(govuk_link_to(planning_application.reference, planning_application_path(planning_application), no_visited_state: true)) },
170+
{ text: planning_application.full_address }
171+
],
172+
right: [
173+
{ label: "Application information", href: planning_application_information_path(planning_application) }
174+
],
175+
toggle: {
176+
condensed_text: "Show proposal description",
177+
expanded_text: "Hide proposal description",
178+
content: content_tag(:p, planning_application.description, class: "govuk-!-margin-0")
179+
}
180+
)
181+
```

0 commit comments

Comments
 (0)