Component Contract
Modal
Dialog overlay for confirmations, disclaimers, terms acceptance, and focused interactions within CAAT digital properties.
Live Demo
Standard Modal
Rendered
Large Modal (modal-lg)
Rendered
Confirmation Dialog
Rendered
Scrollable Modal
Rendered
Code sample
HTML
<!-- Trigger button -->
<button type="button" class="caat-button caat-button--primary"
data-bs-toggle="modal" data-bs-target="#myModal">
Open Modal
</button>
<!-- Modal -->
<div class="modal fade caat-modal" id="myModal" tabindex="-1"
aria-labelledby="myModalLabel" aria-modal="true" role="dialog"
data-analytics-component="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header caat-modal__header">
<h5 class="modal-title" id="myModalLabel">Modal Title</h5>
<button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body caat-modal__body">
<p>Modal body content (rich text).</p>
</div>
<div class="modal-footer caat-modal__footer">
<button type="button" class="caat-button caat-button--outline-primary"
data-bs-dismiss="modal">Cancel</button>
<button type="button"
class="caat-button caat-button--primary">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Large modal — add .modal-lg -->
<div class="modal-dialog modal-lg">…</div>
<!-- Scrollable modal — add .modal-dialog-scrollable -->
<div class="modal-dialog modal-dialog-scrollable">…</div>
<!-- Small modal — add .modal-sm -->
<div class="modal-dialog modal-sm">…</div>
<!-- Extra-large modal — add .modal-xl -->
<div class="modal-dialog modal-xl">…</div>
1. Component Summary
| Component name | Modal |
|---|---|
| AEM component name | caat/components/modal |
| Recommended implementation | Custom AEM component wrapping Bootstrap 5 modal |
| Component group | CAAT Design System |
| Purpose | Dialog overlay for confirmations, disclaimers, terms acceptance, and focused interactions |
| Status | New |
Primary use cases
- Confirmation dialogs — "Are you sure?" prompts before destructive actions
- Disclaimers — pension estimate disclaimers and legal notices
- Terms acceptance — displaying terms and conditions requiring acknowledgement
- Focused interactions — isolating a task (e.g. beneficiary selection) without leaving the page
Non-goals
- Not for displaying simple alerts or notifications (use the Alerts component)
- Not for multi-step wizards — use a dedicated stepper or page flow instead
- Not for image galleries or media lightboxes
- Should not be triggered automatically on page load without user action
2. Design System Source / Token Dependencies
| Token | Value (reference) | Purpose |
|---|---|---|
--caat-blue-900 | #003750 | Modal title text colour |
--caat-blue-700 | #0b5a80 | Primary action button background |
--caat-grey-200 | #dfe6ef | Modal header/footer border colour |
--caat-grey-100 | #e8ecf1 | Modal footer background |
--caat-focus | 0 0 0 .25rem rgba(47,149,210,.35) | Focus ring on interactive elements within modal |
--caat-font-primary | "Libre Franklin", Arial, sans-serif | Modal typeface |
--caat-shadow-lg | 0 1rem 3rem rgba(0,0,0,.175) | Modal elevation shadow |
Size variants
- Small (
.modal-sm) — max-width 300px, for simple confirmations - Default — max-width 500px, for standard dialogs
- Large (
.modal-lg) — max-width 800px, for rich content like disclaimers - Extra-large (
.modal-xl) — max-width 1140px, for complex layouts
3. Authoring Fields
| Field | Type | Required | Description |
|---|---|---|---|
| Trigger button label | Textfield | Yes | Text displayed on the button that opens the modal |
| Modal title | Textfield | Yes | Title displayed in the modal header; also used for aria-labelledby |
| Modal body | Rich text | Yes | Main content area supporting headings, lists, paragraphs, and links |
| Footer actions | Multifield | No | One or more action buttons (label, style variant, action type) |
| Size | Dropdown | Yes | Modal size: sm, default, lg, xl |
| Scrollable | Toggle/Switch | No | Enables .modal-dialog-scrollable for long content |
4. Validation Rules
| Field | Rule |
|---|---|
| Trigger button label | Required, max 80 characters |
| Modal title | Required, max 120 characters |
| Modal body | Required, must contain at least one text node |
| Footer actions | If provided, at least one button must have a label |
| Size | Required, must be one of: sm, default, lg, xl |
5. Content Guidance
Do
- Use clear, action-oriented button labels (e.g. "Confirm Enrolment", "Accept Terms")
- Keep modal titles concise and descriptive of the action or content
- Provide a clear way to dismiss without committing (Cancel / Close button)
- Use scrollable modals for legal content that exceeds the viewport
Don't
- Don't nest modals within modals
- Don't use modals for information that should be inline on the page
- Don't auto-open modals on page load without user-initiated action
- Don't use vague button labels like "OK" or "Click here"
- Don't put critical, required-reading content only in a modal — ensure it is also accessible inline
6. Semantic HTML
<!-- Trigger -->
<button type="button" class="caat-button caat-button--primary"
data-bs-toggle="modal" data-bs-target="#modalId">
${triggerLabel}
</button>
<!-- Modal -->
<div class="modal fade caat-modal" id="${modalId}" tabindex="-1"
aria-labelledby="${modalId}Label" aria-modal="true" role="dialog"
data-analytics-component="modal">
<div class="modal-dialog ${sizeClass} ${scrollableClass}">
<div class="modal-content">
<div class="modal-header caat-modal__header">
<h5 class="modal-title" id="${modalId}Label">${title}</h5>
<button type="button" class="btn-close"
data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body caat-modal__body">
${bodyRichText @ context='html'}
</div>
<div class="modal-footer caat-modal__footer" data-sly-test="${footerActions}">
<sly data-sly-list="${footerActions}">
<button type="button"
class="caat-button caat-button--${item.variant}"
data-analytics-label="${item.analyticsLabel}">
${item.label}
</button>
</sly>
</div>
</div>
</div>
</div>
7. CSS Contract
| Class | Element | Purpose |
|---|---|---|
.caat-modal | Root wrapper | Wraps the Bootstrap .modal; applies CAAT token overrides (shadow, border-radius, font) |
.caat-modal__header | Header | Applies CAAT title colour (--caat-blue-900), bottom border using --caat-grey-200 |
.caat-modal__body | Body | Applies CAAT typography tokens, padding, and rich-text content styling |
.caat-modal__footer | Footer | Applies footer background (--caat-grey-100), top border, button alignment |
CSS file
/assets/css/components/modal.css
All custom styles must use CAAT design tokens. No hard-coded colour values. The .caat-modal class wraps Bootstrap's .modal to layer CAAT-specific visual overrides without breaking Bootstrap functionality.
8. Accessibility
| Requirement | Implementation |
|---|---|
role="dialog" | Applied to the .modal root element |
aria-modal="true" | Indicates the modal is a modal dialog, preventing interaction with background content |
aria-labelledby | Points to the modal title element's id so assistive technology announces the dialog purpose |
| Focus trap | Bootstrap provides built-in focus trap. Tab and Shift+Tab cycle through focusable elements within the modal only |
| Return focus on close | When the modal closes, focus must return to the trigger element that opened it (Bootstrap default behaviour) |
| Escape to close | Pressing Escape closes the modal (Bootstrap default). Can be disabled via data-bs-keyboard="false" if needed |
| Close button | The .btn-close element must have aria-label="Close" |
| Background scroll lock | Page scrolling is disabled while the modal is open (Bootstrap default) |
Keyboard interaction
- Tab — move forward through focusable elements inside the modal
- Shift + Tab — move backward through focusable elements
- Escape — close the modal and return focus to the trigger
- Enter / Space — activate the focused button
9. SEO
Transient / structural UI with no indexable content of its own — no SEO markup required. Ensure any content this component wraps or reveals stays server-rendered and crawlable.
10. Analytics
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 Notes
- Component path:
/apps/caat/components/modal - Resource type:
caat/components/modal - Component group: CAAT Design System
- Sling Model: Create a Sling Model to expose trigger label, title, body, footer actions, size, and scrollable properties.
- Unique ID generation: Each modal instance must generate a unique
id(e.g. based on component path hash) to support multiple modals on one page. - Client library:
caat.components.modal— includes CSS and JS for analytics event wiring. - Policies: Configure allowed rich-text formats for the body field in template policies.
12. Dialog Model (AEM Touch UI)
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/modal/_cq_dialog/.content.xml (pseudo-YAML)
jcr:root:
sling:resourceType: cq/gui/components/authoring/dialog
jcr:title: Modal
content:
sling:resourceType: granite/ui/components/coral/foundation/container
items:
tabs:
sling:resourceType: granite/ui/components/coral/foundation/tabs
items:
# ── Content tab ──────────────────────────────
content:
jcr:title: Content
items:
triggerLabel:
sling:resourceType: granite/ui/components/coral/foundation/form/textfield
name: ./triggerLabel
fieldLabel: Trigger button label
required: true
maxlength: 80
modalTitle:
sling:resourceType: granite/ui/components/coral/foundation/form/textfield
name: ./modalTitle
fieldLabel: Modal title
required: true
maxlength: 120
modalBody:
sling:resourceType: granite/ui/components/coral/foundation/form/richtext
name: ./modalBody
fieldLabel: Modal body
required: true
footerActions:
sling:resourceType: granite/ui/components/coral/foundation/form/multifield
name: ./footerActions
fieldLabel: Footer actions
composite: 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: Button label
required: true
variant:
sling:resourceType: granite/ui/components/coral/foundation/form/select
name: ./variant
fieldLabel: Button style
items:
- { text: Primary, value: primary }
- { text: Outline Primary, value: outline-primary }
analyticsLabel:
sling:resourceType: granite/ui/components/coral/foundation/form/textfield
name: ./analyticsLabel
fieldLabel: Analytics label
# ── Style tab ────────────────────────────────
style:
jcr:title: Style
items:
size:
sling:resourceType: granite/ui/components/coral/foundation/form/select
name: ./size
fieldLabel: Modal size
required: true
items:
- { text: Small, value: sm }
- { text: Default, value: default }
- { text: Large, value: lg }
- { text: Extra Large, value: xl }
defaultValue: default
scrollable:
sling:resourceType: granite/ui/components/coral/foundation/form/switch
name: ./scrollable
fieldLabel: Scrollable body
checked: false
# ── Behaviour tab ────────────────────────────
behaviour:
jcr:title: Behaviour
items:
staticBackdrop:
sling:resourceType: granite/ui/components/coral/foundation/form/checkbox
name: ./staticBackdrop
text: Static backdrop (prevent close on backdrop click)
analyticsTracking:
sling:resourceType: granite/ui/components/coral/foundation/form/switch
name: ./analyticsTracking
fieldLabel: Analytics tracking
checked: true
13. QA Acceptance Checklist
- All size variants (sm, default, lg, xl) render correctly with expected max-widths.
- Scrollable modal scrolls body content while header and footer remain fixed.
- Modal opens and closes via trigger button, close button, backdrop click, and Escape key.
- Focus is trapped within the modal when open — Tab does not leave the dialog.
- Focus returns to the trigger button when the modal closes.
-
role="dialog",aria-modal="true", andaria-labelledbyare present and correct. - Screen reader announces the modal title when opened.
- Background page scroll is locked while modal is open.
- Analytics events (
modal-open,modal-close,modal-confirm) fire correctly to the data layer. - Multiple modals on the same page each have unique IDs and function independently.
- Cross-browser tested: Chrome, Firefox, Safari, Edge.
- Responsive: modal is usable on mobile viewports without horizontal overflow.
14. Definition of Done
| Area | Done means |
|---|---|
| Design | Figma component matches this contract. All size variants (sm, default, lg, xl) and scrollable option are documented. Tokens are used — no hard-coded colour values. |
| Development | AEM component created at /apps/caat/components/modal. HTL template emits semantic HTML per Section 6. CSS uses design tokens per Section 7. JS wires Bootstrap modal events to analytics data layer. Sling Model exposes all authoring fields. 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). Focus trap and return-focus verified with screen reader. |
| Launch | Component deployed to AEM production. Content authors trained on authoring dialog. Analytics dashboards confirmed receiving modal-open, modal-close, and modal-confirm events. |