Component Contract
Tabs
Organize related content into switchable panels — reduce page length while keeping content discoverable.
Live Demo
Standard Horizontal Tabs
Rendered
CAAT DBplus Plan Overview
The CAAT DBplus pension plan is a defined benefit plan that provides predictable, lifetime retirement income. Unlike defined contribution plans, your benefit is based on a formula — not market performance — so you always know what to expect in retirement.
Key features include immediate vesting, portability between participating employers, and conditional inflation protection.
Benefits for Members
As a CAAT DBplus member, you receive a secure, predictable pension income for life. Your contributions are matched by your employer, and your benefit grows with every year of service.
- Immediate vesting — your pension is yours from day one
- Survivor benefits for your spouse or beneficiary
- Conditional inflation protection to help maintain purchasing power
- Early retirement options available from age 55
Information for Employers
Offering CAAT DBplus helps you attract and retain talent with a best-in-class pension benefit — without the governance burden of running your own plan.
Contribution rates are stable and predictable. CAAT manages all investments, administration, and regulatory compliance on your behalf.
Pill-Style Tabs
Rendered
Both you and your employer contribute to your CAAT DBplus pension. Contribution rates are set by the plan and are designed to keep the plan fully funded while providing a meaningful retirement benefit.
Your retirement income is calculated using a formula based on your contributions and years of service. You can begin receiving an unreduced pension at age 60, or opt for a reduced pension as early as age 55.
If you pass away before or after retirement, your spouse or designated beneficiary may be eligible for a survivor pension or a lump-sum death benefit, depending on your elections and membership status.
Vertical Tabs
Rendered
Who is Eligible?
All employees of participating employers are eligible to join CAAT DBplus. There is no minimum age or service requirement — you can begin earning pension benefits from your first day of employment.
How to Enrol
Enrolment is handled by your employer's HR or payroll department. Once enrolled, contributions are automatically deducted from your pay and your employer makes matching contributions on your behalf.
Options When Leaving
If you leave a participating employer, your earned pension benefit stays with CAAT. You can choose to leave it in the plan and collect it at retirement, or explore transfer options if available.
States
Follows the Interaction States matrix. Tab buttons implement default, hover, active (selected), and focus-visible. The static is-* classes mirror the live pseudo-class states for documentation and Figma export.
Rendered — underline tabs
Rendered — pill tabs
Code sample
HTML
<!-- Standard horizontal tabs -->
<div class="caat-tabs" data-analytics-component="tabs">
<ul class="nav nav-tabs" id="myTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab1-tab"
data-bs-toggle="tab"
data-bs-target="#tab1-panel"
type="button" role="tab"
aria-controls="tab1-panel"
aria-selected="true"
data-analytics-label="tab-1">
Tab Label
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active"
id="tab1-panel" role="tabpanel"
aria-labelledby="tab1-tab" tabindex="0">
Panel content here.
</div>
</div>
</div>
<!-- Pill variant — replace .nav-tabs with .nav-pills -->
<ul class="nav nav-pills" role="tablist">…</ul>
<!-- Vertical variant — add flex + aria-orientation -->
<div class="caat-tabs caat-tabs--vertical d-flex">
<ul class="nav nav-tabs flex-column me-3"
role="tablist" aria-orientation="vertical">…</ul>
<div class="tab-content flex-grow-1">…</div>
</div>
1. Component Summary
| Component name | Tabs |
|---|---|
| AEM component name | caat/components/tabs |
| Recommended implementation | Extend the AEM Core Tabs component (core/wcm/components/tabs) |
| Component group | CAAT Design System |
| Purpose | Organize related content into switchable panels; reduce page length while keeping content discoverable |
| Existing AEM components replaced | Tabs (#50). Replaces 1 existing component — rebuild required. |
| AEM Triage | Rebuild |
Primary use cases
- Plan details pages — separating member, employer, and general information into parallel views
- Account dashboard — switching between contribution history, benefit projections, and personal info
- Product comparison — side-by-side feature comparison across plan types
- Service pages — organizing content by audience or topic without long scroll
Non-goals
- This component is not for sequential workflows — use a Stepper component for multi-step processes
- It is not for primary site navigation — use the Navigation component instead
- Do not use tabs to hide critical information that all users must read — such content must be visible without interaction
2. Design System Source / Token Dependencies
| Token | Value (reference) | Purpose |
|---|---|---|
--caat-blue-900 | #003750 | Active tab text colour, bottom border indicator |
--caat-green | #55a546 | Active pill background |
--caat-blue-100 | #e7f4fb | Tab hover background tint |
--caat-grey-200 | #dfe6ef | Tab border colour, panel divider |
--caat-focus | 0 0 0 .25rem rgba(47,149,210,.35) | Focus ring on tab buttons |
--caat-ink | #102637 | Panel body text colour |
--caat-font-primary | "Libre Franklin", Arial, sans-serif | Tab typeface |
Visual variants
- Standard tabs (
.nav-tabs) — underlined active indicator, bordered content panel - Pills (
.nav-pills) — filled capsule-style active indicator - Vertical (
.caat-tabs--vertical) — side-navigation layout with tabs stacked vertically
3. Authoring Fields
Content tab
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| Tab label (per item) | Text field | Yes | — | Visible text on the tab button. Keep concise (1–3 words). |
| Panel content (per item) | Rich text editor | Yes | — | Supports paragraphs, lists, links, images, and embedded components. |
| Icon (per item) | Icon picker | No | — | Optional Bootstrap icon displayed before the tab label. |
| Active by default | Toggle (per item) | No | First item | Designates which tab panel is visible on initial page load. Only one item should be active. |
Style tab
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| Variant | Dropdown | Yes | Standard | standard | pills | vertical |
| Full-width tabs | Toggle | No | Off | When on, tabs stretch to fill the available width equally (.nav-justified). |
Behaviour tab
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| Analytics tracking | Toggle | No | On | When on, emits data-analytics-* attributes and fires tab-switch events. |
| Deep-link tabs | Toggle | No | Off | When on, appends a hash to the URL when a tab is activated, enabling direct linking to a specific tab. |
4. Authoring Validation Rules
- At least two tab items are required. A single-tab interface provides no benefit — use a standard content block instead.
- Tab label is required for every item. The dialog must not allow saving a tab without a label.
- Maximum of 7 tabs recommended. More than 7 creates cognitive overload and responsive layout issues. The dialog should display a warning above this threshold.
- Only one tab may be marked "active by default." If none is selected, the first tab is active. If multiple are checked, only the first applies.
- Authors must never hide essential action items or legal disclosures in non-default tab panels that members are required to acknowledge.
5. Content Guidance
Good examples
- Short, scannable tab labels: "Overview", "Benefits", "Eligibility"
- Parallel content structure — each panel covers a similar scope for its topic
- Use when audiences differ: "For Members" / "For Employers"
- Keep panels roughly equal in content length
- Ensure the default tab contains the most commonly sought information
Avoid
- Long tab labels: "Information About Your Retirement Benefits"
- More than 7 tabs — consider a different pattern (e.g., accordion or sub-pages)
- Nesting tabs within tabs — use a flat hierarchy
- Using tabs for sequential content that must be read in order
- Extremely uneven panel lengths (one sentence vs. one full page)
6. Expected Semantic HTML Output
Standard tabs
<div class="caat-tabs" data-analytics-component="tabs">
<ul class="nav nav-tabs" id="planTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab1-tab"
data-bs-toggle="tab"
data-bs-target="#tab1-panel"
type="button" role="tab"
aria-controls="tab1-panel"
aria-selected="true">
Tab Label
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab2-tab"
data-bs-toggle="tab"
data-bs-target="#tab2-panel"
type="button" role="tab"
aria-controls="tab2-panel"
aria-selected="false">
Tab Label 2
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active"
id="tab1-panel" role="tabpanel"
aria-labelledby="tab1-tab" tabindex="0">
<p>Panel content.</p>
</div>
<div class="tab-pane fade"
id="tab2-panel" role="tabpanel"
aria-labelledby="tab2-tab" tabindex="0">
<p>Panel content.</p>
</div>
</div>
</div>
Vertical tabs
<div class="caat-tabs caat-tabs--vertical d-flex"
data-analytics-component="tabs">
<ul class="nav nav-tabs flex-column me-3"
id="vertTabs" role="tablist"
aria-orientation="vertical">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="vtab1-tab"
data-bs-toggle="tab"
data-bs-target="#vtab1-panel"
type="button" role="tab"
aria-controls="vtab1-panel"
aria-selected="true">
Tab Label
</button>
</li>
</ul>
<div class="tab-content flex-grow-1">
<div class="tab-pane fade show active"
id="vtab1-panel" role="tabpanel"
aria-labelledby="vtab1-tab" tabindex="0">
Panel content.
</div>
</div>
</div>
Tab with icon
<button class="nav-link" role="tab" …>
<i class="bi bi-people me-1" aria-hidden="true"></i>
Members
</button>
7. CSS Contract
The tabs component relies on the global token layer defined in /assets/css/tokens.css.
All component-specific styles live in
/assets/css/components/tabs.css.
Base styles
.caat-tabs {
font-family: var(--caat-font-primary, "Libre Franklin", Arial, sans-serif);
}
.caat-tabs .nav-tabs {
border-bottom-color: var(--caat-grey-200, #e9ecef);
}
.caat-tabs .nav-tabs .nav-link {
font-weight: 600;
color: var(--caat-ink, #212529);
border: none;
border-bottom: 3px solid transparent;
padding: .75rem 1.25rem;
}
.caat-tabs .nav-tabs .nav-link:hover {
background-color: var(--caat-blue-100, #e6f0f7);
border-bottom-color: var(--caat-grey-200, #e9ecef);
}
.caat-tabs .nav-tabs .nav-link.active {
color: var(--caat-blue-900, #003750);
border-bottom-color: var(--caat-blue-900, #003750);
background-color: transparent;
}
/* Pills variant */
.caat-tabs .nav-pills .nav-link.active {
background-color: var(--caat-green, #00874c);
color: #fff;
}
/* Tab content panel */
.caat-tabs .tab-content {
padding: 1.5rem 0;
color: var(--caat-ink, #212529);
}
/* Focus ring */
.caat-tabs .nav-link:focus-visible {
box-shadow: var(--caat-focus);
outline: 0;
}
/* Vertical variant */
.caat-tabs--vertical .nav-tabs {
border-bottom: none;
border-right: 1px solid var(--caat-grey-200, #e9ecef);
}
.caat-tabs--vertical .nav-tabs .nav-link {
border-bottom: none;
border-right: 3px solid transparent;
text-align: left;
}
.caat-tabs--vertical .nav-tabs .nav-link.active {
border-right-color: var(--caat-blue-900, #003750);
border-bottom-color: transparent;
}
var(--caat-blue-900))
so that theme changes propagate automatically.
8. Accessibility Requirements
| Requirement | Acceptance criteria |
|---|---|
| Tab list role | The <ul> containing tab buttons must have role="tablist". |
| Tab role | Each tab button must have role="tab". |
| Tab panel role | Each content panel must have role="tabpanel" and aria-labelledby pointing to its corresponding tab button. |
| aria-selected | The active tab must have aria-selected="true"; all others must have aria-selected="false". |
| aria-controls | Each tab button must have aria-controls pointing to the id of its associated panel. |
| Keyboard — Arrow keys | Left/Right arrow keys move focus between tabs in horizontal mode. Up/Down arrows in vertical mode. Focus wraps at boundaries. |
| Keyboard — Activation | Focused tab is activated on Enter or Space (or automatically on focus — follow Bootstrap default). |
| Focus state | A visible focus ring (var(--caat-focus)) must appear on :focus-visible. The ring must meet a 3 : 1 contrast ratio against the background. |
| Vertical orientation | Vertical tabs must include aria-orientation="vertical" on the tablist so assistive technology announces correct arrow key behaviour. |
| Colour contrast | Text-to-background contrast must meet WCAG 2.1 AA (4.5 : 1 for normal text) in all tab states (default, hover, active, focus). |
9. SEO Requirements
- All tab panel content must be present in the initial HTML response — do not lazy-load panel content via AJAX, as search engines must be able to index all panels.
- Use semantic headings within panels where appropriate to support document outline.
- If deep-linking is enabled, ensure each tab panel URL is crawlable and renders the correct panel content for social sharing and link previews.
- Avoid duplicating the same content across multiple tab panels — each panel should contain unique information.
10. Analytics Requirements
Analytics are captured through the shared data-analytics-component attribute on the component root (emitted automatically) and reported to the central data layer. Event names and payloads follow the organisation's standard analytics schema — defined centrally with the analytics team, not invented per component — so no bespoke tracking is specified here.
11. AEM Implementation Notes
Existing component mapping
This component replaces:
- Tabs (#50) — existing tab component requiring rebuild to meet accessibility and design token standards
AEM triage
Rebuild The existing Tabs component (#50) must be rebuilt to use Bootstrap 5 tab markup, CAAT design tokens, proper ARIA roles, and analytics data attributes.
Proxy / extend guidance
Create a proxy component at /apps/caat/components/tabs that extends the
AEM Core Tabs component (core/wcm/components/tabs) where feasible.
Override the HTL template to emit CAAT class names, role attributes, and data-analytics attributes.
Client library structure
- Category:
caat.components.tabs - Dependencies:
caat.base(tokens + base styles) - CSS:
tabs.css - JS:
tabs.js(analytics wiring, deep-link hash handling, tab-switch event tracking)
Suggested file structure
tabs/
├── _cq_dialog/
│ └── .content.xml # Touch UI dialog
├── _cq_editConfig.xml # Edit configuration
├── .content.xml # Component node definition
├── tabs.html # HTL template
├── clientlibs/
│ ├── .content.xml
│ ├── css/
│ │ └── tabs.css
│ └── js/
│ └── tabs.js
└── README.md # Dev notes
12. Dialog Field Model
Simplified model — shows field intent and Granite UI resource types, not the literal cq:dialog node tree (e.g. select options are authored as child items nodes).
# caat/components/tabs — Touch UI Dialog
# _cq_dialog/.content.xml (YAML representation)
dialog:
jcr:primaryType: nt:unstructured
sling:resourceType: cq/gui/components/authoring/dialog
content:
jcr:primaryType: nt:unstructured
sling:resourceType: granite/ui/components/coral/foundation/container
items:
tabs:
jcr:primaryType: nt:unstructured
sling:resourceType: granite/ui/components/coral/foundation/tabs
items:
# ── Content tab ──────────────────────────────
content:
jcr:title: Content
items:
tabItems:
sling:resourceType: granite/ui/components/coral/foundation/form/multifield
name: ./tabItems
fieldLabel: Tab items
composite: true
required: true
items:
field:
sling:resourceType: granite/ui/components/coral/foundation/container
items:
label:
sling:resourceType: granite/ui/components/coral/foundation/form/textfield
name: ./label
fieldLabel: Tab label
required: true
panelContent:
sling:resourceType: granite/ui/components/coral/foundation/form/richtext
name: ./panelContent
fieldLabel: Panel content
required: true
icon:
sling:resourceType: granite/ui/components/coral/foundation/form/textfield
name: ./icon
fieldLabel: Icon (Bootstrap Icons class)
emptyText: "e.g. bi-people"
activeByDefault:
sling:resourceType: granite/ui/components/coral/foundation/form/checkbox
name: ./activeByDefault
text: Active by default
# ── Style tab ────────────────────────────────
style:
jcr:title: Style
items:
variant:
sling:resourceType: granite/ui/components/coral/foundation/form/select
name: ./variant
fieldLabel: Variant
required: true
items:
- { text: Standard (underline), value: standard }
- { text: Pills, value: pills }
- { text: Vertical, value: vertical }
fullWidth:
sling:resourceType: granite/ui/components/coral/foundation/form/switch
name: ./fullWidth
fieldLabel: Full-width tabs
checked: false
# ── Behaviour tab ────────────────────────────
behaviour:
jcr:title: Behaviour
items:
deepLink:
sling:resourceType: granite/ui/components/coral/foundation/form/checkbox
name: ./deepLink
text: Enable deep-linking (hash URL)
analyticsTracking:
sling:resourceType: granite/ui/components/coral/foundation/form/switch
name: ./analyticsTracking
fieldLabel: Analytics tracking
checked: true
13. QA Acceptance Checklist
- All three variants (standard, pills, vertical) render correctly with expected styles and spacing.
- Tab switching works — clicking a tab shows its panel and hides others.
- Keyboard navigation works: Arrow keys move between tabs, Enter/Space activates.
-
role="tablist",role="tab", androle="tabpanel"are present and correct. -
aria-selected,aria-controls, andaria-labelledbyupdate correctly on tab switch. - Vertical tabs include
aria-orientation="vertical"on the tablist. - Deep-link hash updates the URL and activating a tab via hash on page load works correctly.
- Analytics
data-analytics-*attributes are present and tab-switch events fire to the data layer. - Responsive behaviour: tabs stack or scroll horizontally on small viewports without content clipping.
- Focus ring is visible and meets 3 : 1 contrast on all tab buttons.
14. Definition of Done
| Area | Done means |
|---|---|
| Design | Figma component matches this contract. All variants (standard, pills, vertical) are documented. Tokens are used — no hard-coded colour values. |
| Development | AEM proxy component created extending Core Tabs. HTL template emits semantic HTML per Section 6. CSS uses design tokens. JS wires analytics tab-switch events and optional deep-link hash. Legacy Tabs (#50) replaced. Unit tests pass. |
| QA | All items in the QA Acceptance Checklist (Section 13) pass. Manual accessibility audit completed. Cross-browser tested (Chrome, Firefox, Safari, Edge). Screen reader tested with NVDA and VoiceOver. |
| Launch | Component deployed to AEM production. Legacy Tabs component deprecated and migration plan communicated. Content authors trained. Analytics dashboards confirmed receiving tab-switch events. |