Component Contract
Side Navigation
Left-hand navigation showing only the current Level 3 branch’s Level 4 pages. Users move to higher levels through the mega navigation or breadcrumb (see the Navigation Model), so L2 and L3 links are not repeated here.
Live Demo
How it fits the navigation flow
| Step | Surface | What the user sees |
|---|---|---|
| 1 | Mega nav | Opens an L1 panel, clicks an L3 link (the deepest level the mega nav shows) — e.g. Pension solutions → Why join → What members get |
| 2 | L3 page | Lands on the L3 page: the breadcrumb shows the trail and the side nav lists only that page’s L4 children |
| 3 | Side nav | Clicks an L4 link (e.g. Secure lifetime retirement income) |
| 4 | L4 page | The same flat L4 list persists and the current page is marked aria-current="page"; the breadcrumb extends to the L4 page |
L4 page (current branch only)
An L4 page (Secure lifetime retirement income) inside Pension solutions → Why join → What members get. The side nav lists only the L4 pages beneath What members get. Below 992 px it collapses behind a prominent “In this section” disclosure toggle.
Secure lifetime retirement income
Page content for the current L4 page. The side nav includes its L4 siblings and marks only the page itself with aria-current="page". Higher-level items are available through the mega navigation and breadcrumb.
L3 page (its L4 children)
What members get
An L3 page with children shows its L4 destinations as a flat list. No item is current until the user opens an L4 page. If an L3 page has no visible L4 children, the component does not render.
States
Link interaction states per the Interaction States foundation. Hover and focus are shown statically via .is-* mirrors of the live :hover / :focus-visible rules; the current L4 page is carried by aria-current="page" (weight + rail + tint — never colour alone).
1. Summary
The Side Navigation orients users within the current L3 branch by listing only its visible L4 children. It appears on an L3 page when that page has L4 children and persists on those L4 pages. It does not repeat L2 or L3 destinations; users move to those levels through the mega navigation or breadcrumb.
| Status | Ready |
|---|---|
| Type | Navigation / wayfinding |
| JS | side-nav.js — mobile disclosure toggle only (progressive enhancement: without JS the nav renders fully expanded). Desktop is pure CSS. |
| Used with | Breadcrumb and Navigation — those surfaces handle L1–L3 movement; the side nav is the focused hand-off from the current L3 page to its L4 children |
2. Design Tokens
| Token | Role |
|---|---|
--caat-color-text / --caat-color-text-heading | Link / title colour |
--caat-color-state-hover-bg | Row hover background |
--caat-color-bg-brand-subtle | Current-item background |
--caat-color-action-primary | Current-item left rail |
--caat-color-border · --caat-border-2 | Branch-label divider and current-page rail |
--caat-color-focus-ring | Keyboard focus |
--caat-font-size-xs / --caat-font-size-sm / --caat-font-size-md · --caat-weight-semibold / --caat-weight-bold / --caat-weight-extrabold · --caat-space-1 / --caat-space-2 / --caat-space-3 | Type and rhythm |
3. Authoring Fields
Not authored per page — the component derives its content from the current L3 page or, on an L4 page, its L3 parent. The only authorable options:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| Sticky | Checkbox | No | On | Sticky below the header on desktop (--sticky) |
| Hide pages | Pathfield multi | No | — | Exclude specific child pages (mirrors "Hide in navigation") |
4. Validation Rules
| Rule | Behaviour |
|---|---|
| Current L3 branch only | Render only the visible L4 children of the current L3 page or the current L4 page’s L3 parent |
| Hide empty navigation | Do not render the component when an L3 page has no visible L4 children |
| No L5 | The IA stops at L4; deeper pages are a content-architecture error and are not rendered |
| Current marking | On L4 pages, exactly one link has aria-current="page"; no ancestor link is rendered |
5. Content Guidance
- Labels are page titles (or their short navigation titles) — never rewritten copy.
- Keep L4 lists scannable: prefer 4–8 items; if a branch needs substantially more, review the IA.
- The current L3 label provides context as a heading, not a link. The breadcrumb provides the link back to that page.
6. Semantic HTML
<nav class="caat-side-nav caat-side-nav--sticky" aria-label="Section navigation">
<!-- Mobile disclosure toggle (hidden on desktop; side-nav.js wires it) -->
<button class="caat-side-nav__toggle" type="button"
aria-expanded="false" aria-controls="sideNavBody">
<span>In this section: What members get</span> <i class="bi bi-chevron-down" aria-hidden="true"></i>
</button>
<div class="caat-side-nav__body" id="sideNavBody">
<h2 class="caat-side-nav__heading">What members get</h2> <!-- current L3 label, not a link -->
<ul class="caat-side-nav__list">
<li><a class="caat-side-nav__link" href="…">Value</a></li>
<li><a class="caat-side-nav__link" href="…" aria-current="page">Secure lifetime retirement income</a></li>
<li><a class="caat-side-nav__link" href="…">Early retirement options</a></li>
</ul>
</div>
</nav>
Place inside the page grid: col-lg-3 for the nav, col-lg-9 for content. Below 992 px the grid stacks the nav above the content (Responsive foundation). Each rendered instance must use a unique aria-controls / body id pair.
7. CSS Contract
All styles in /assets/css/components/side-nav.css.
| Class | Purpose |
|---|---|
.caat-side-nav | Root <nav> |
.caat-side-nav--sticky | Modifier: sticky below header on desktop |
.caat-side-nav__heading | Current L3 branch label; contextual heading, not a link |
.caat-side-nav__list | Flat list containing only the current branch’s L4 pages |
.caat-side-nav__link | Nav link; [aria-current] styles the active rail |
.caat-side-nav__toggle / __body | Mobile disclosure: prominent “In this section” button (hidden ≥ 992 px) and the collapsible wrapper it controls |
.caat-side-nav--collapsible / .is-open | Added by side-nav.js; only --collapsible allows CSS to collapse the body (no-JS fallback = expanded) |
8. Accessibility
| Requirement | Implementation |
|---|---|
| Landmark | <nav aria-label="Section navigation"> — unique label, distinct from "Main navigation" and "Breadcrumb" |
| Current page | aria-current="page" on the current L4 link; colour is never the only cue (weight + rail) |
| Keyboard | Plain links — Tab order follows the visual order; visible focus ring per Interaction States |
| Structure | A heading identifies the current L3 branch and a single <ul> contains its L4 links; no ARIA tree role is needed |
| Mobile disclosure | The toggle is a real <button> with aria-expanded + aria-controls; it names the current L3 branch (“In this section: What members get”), is ≥ 44 px tall, uses a unique controlled body id, and is deliberately prominent |
9. SEO
Server-rendered crawlable links connect the sibling L4 pages within the current L3 branch. Higher-level internal links remain in the breadcrumb and global navigation.
10. Analytics
data-analytics-component="side-nav"
data-analytics-label="{link text}" <!-- per link, kebab-case -->
data-analytics-level="4"
11. AEM Implementation Notes
- Resource type:
caat/components/side-nav; include in an L3 page template when visible L4 children exist and on its L4 page template — not authorable into other parsys positions. - Tree is computed from the current page path: use the current page when it is L3, otherwise ascend from the current L4 page to its L3 parent; list only that L3 page’s visible children.
- Navigation titles (
jcr:title/ nav title override) supply labels. - Include the
side-nav.jsclientlib only on templates where the side nav can render; it adds the mobile disclosure behavior while preserving the expanded no-JS fallback.
12. Dialog Model
Minimal — content is tree-derived, not authored.
{
"sticky": { "sling:resourceType": "granite/ui/components/coral/foundation/form/checkbox", "name": "./sticky", "text": "Stick below header on desktop", "checked": true },
"hidePages": { "sling:resourceType": "granite/ui/components/coral/foundation/form/multifield", "name": "./hidePages", "fieldLabel": "Hide pages" }
}
13. QA Acceptance Checklist
- Renders on an L3 page only when it has visible L4 children, and on those L4 pages; absent elsewhere.
- Exactly one
aria-current="page"on an L4 page; no L2 or L3 navigation links render. - Only the current L3 branch’s L4 items appear in one flat list.
- Below 992 px: collapses behind the “In this section” toggle (unique
aria-controlstarget,aria-expandedreflects state); without JS the nav renders expanded; sticky on desktop when enabled. - Focus ring visible on every link; hover/current styles match Interaction States and never rely on colour alone.
- Flow verified: mega nav L3 link → L3 page shows only its L4 children → L4 page keeps the same flat list and marks the current page.
- Labels match page navigation titles; hidden pages excluded.
14. Definition of Done
- Tree derivation verified against the content architecture (current L3 branch’s visible L4 children only).
- Breadcrumb + side nav present together where the side nav is eligible; mega navigation unchanged.
- Mobile disclosure and no-JS expanded fallback verified at the responsive breakpoint.
- QA checklist passes on an L3 page with children, an L3 page without children, an L4 page, and a hidden-page case.