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 nameModal
AEM component namecaat/components/modal
Recommended implementationCustom AEM component wrapping Bootstrap 5 modal
Component groupCAAT Design System
PurposeDialog overlay for confirmations, disclaimers, terms acceptance, and focused interactions
StatusNew

Primary use cases

  1. Confirmation dialogs — "Are you sure?" prompts before destructive actions
  2. Disclaimers — pension estimate disclaimers and legal notices
  3. Terms acceptance — displaying terms and conditions requiring acknowledgement
  4. Focused interactions — isolating a task (e.g. beneficiary selection) without leaving the page

Non-goals

  1. Not for displaying simple alerts or notifications (use the Alerts component)
  2. Not for multi-step wizards — use a dedicated stepper or page flow instead
  3. Not for image galleries or media lightboxes
  4. Should not be triggered automatically on page load without user action

2. Design System Source / Token Dependencies

TokenValue (reference)Purpose
--caat-blue-900#003750Modal title text colour
--caat-blue-700#0b5a80Primary action button background
--caat-grey-200#dfe6efModal header/footer border colour
--caat-grey-100#e8ecf1Modal footer background
--caat-focus0 0 0 .25rem rgba(47,149,210,.35)Focus ring on interactive elements within modal
--caat-font-primary"Libre Franklin", Arial, sans-serifModal typeface
--caat-shadow-lg0 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

FieldTypeRequiredDescription
Trigger button labelTextfieldYesText displayed on the button that opens the modal
Modal titleTextfieldYesTitle displayed in the modal header; also used for aria-labelledby
Modal bodyRich textYesMain content area supporting headings, lists, paragraphs, and links
Footer actionsMultifieldNoOne or more action buttons (label, style variant, action type)
SizeDropdownYesModal size: sm, default, lg, xl
ScrollableToggle/SwitchNoEnables .modal-dialog-scrollable for long content

4. Validation Rules

FieldRule
Trigger button labelRequired, max 80 characters
Modal titleRequired, max 120 characters
Modal bodyRequired, must contain at least one text node
Footer actionsIf provided, at least one button must have a label
SizeRequired, 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

ClassElementPurpose
.caat-modalRoot wrapperWraps the Bootstrap .modal; applies CAAT token overrides (shadow, border-radius, font)
.caat-modal__headerHeaderApplies CAAT title colour (--caat-blue-900), bottom border using --caat-grey-200
.caat-modal__bodyBodyApplies CAAT typography tokens, padding, and rich-text content styling
.caat-modal__footerFooterApplies 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

RequirementImplementation
role="dialog"Applied to the .modal root element
aria-modal="true"Indicates the modal is a modal dialog, preventing interaction with background content
aria-labelledbyPoints to the modal title element's id so assistive technology announces the dialog purpose
Focus trapBootstrap provides built-in focus trap. Tab and Shift+Tab cycle through focusable elements within the modal only
Return focus on closeWhen the modal closes, focus must return to the trigger element that opened it (Bootstrap default behaviour)
Escape to closePressing Escape closes the modal (Bootstrap default). Can be disabled via data-bs-keyboard="false" if needed
Close buttonThe .btn-close element must have aria-label="Close"
Background scroll lockPage 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", and aria-labelledby are 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

AreaDone 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.