Component Contract
Forms
Unified form component for contact, enrollment, feedback, and general data collection across CAAT digital properties.
AEM Triage: Rebuild — a single unified form component replaces three legacy implementations.
Live Demo
Variant 1 — Contact Form
Variant 2 — Enrollment / Registration (Multi-Step)
Variant 3 — Simple Feedback Form
States
Follows the Interaction States matrix. Inputs implement default, focus, valid, invalid (error), and disabled. The static is-focus class mirrors the live :focus ring for documentation and Figma export.
Variant 4 — Pension Solutions Form (Account Engagement)
Use this pattern for employer-facing lead-gen forms posted to Account Engagement (Pardot). Includes logo, UTM tracking, honeypot, reCAPTCHA, email opt-in, and privacy consent.
Connect with Pension Solutions
Complete the form below and our team will follow up within 2 business days.
Form Consent, Validation & Follow-Up Patterns
Standard patterns for Account Engagement forms, campaign landing pages, gated content, event registrations, and contact requests.
Consent Language
Place consent near the submit button, not hidden in a footer. Use plain language that explains why CAAT is collecting information and what follow-up may happen.
Email Opt-In
Use an unchecked checkbox for marketing email permission. Do not bundle email consent into the form submission itself.
Required Fields
- Mark required fields with
*in the label and expose "required" to screen readers via.visually-hidden. - Add Province/territory when routing, segmentation, events, or regional follow-up matters.
- Keep phone optional unless a phone call is essential to the service request.
- Add
aria-required="true"to the input, not just the HTMLrequiredattribute.
Validation Copy Patterns
Be specific — tell the user what format is expected, not just "invalid input."
Thank You Page Pattern
A successful form submission should redirect to a dedicated thank-you page or show a persistent success state. Never leave users on a blank form or rely only on a toast notification.
Thank you!
A member of the Pension Solutions team will be in touch with you within 2 business days.
Return to Pension SolutionsThank-you page requirements:
- Confirm the form was received.
- State who will follow up and when, if known.
- Offer a relevant next step (resource link, event registration, etc.).
- Do not repeat personal information back to the user.
- Fire a
form-submit-successanalytics event on page load.
Thank-you page starter HTML
<main class="caat-form__thank-you">
<div class="caat-form__thank-you-icon">
<i class="bi bi-check-circle-fill"></i>
</div>
<h1>Thank you!</h1>
<p>A member of the Pension Solutions team
will be in touch with you soon.</p>
<a href="/pension-solutions"
class="caat-button caat-button--primary">
Return to Pension Solutions
</a>
</main>
1. Summary
| Component name | caat/components/form |
| Consolidates | Contact Us (#26), Generic Form (#38), Core Form (#31) |
| AEM Triage | Rebuild |
| Purpose | Unified form component for contact, enrollment, feedback, and general data collection |
| Variants | Single-step, Multi-step (with progress indicator) |
| Field types | text, email, tel, select, textarea, checkbox, radio, date, file |
2. Design Tokens
Forms consume these semantic tokens — no component-specific tokens are defined.
| Token | Role |
|---|---|
--caat-color-border | Input / select / textarea border |
--caat-color-focus-ring | Focus ring |
--caat-color-state-invalid | Invalid (error) border + message colour |
--caat-green | Valid state accent |
--caat-color-state-selected-bg / -selected-text | Checked radio / checkbox fill + glyph |
--caat-color-state-disabled-bg | Disabled control surface |
--caat-color-text-heading | Field label colour |
--caat-color-text-muted | Help / hint text |
--caat-color-text-link | In-field links |
--caat-color-bg-page | Input background |
--caat-radius | Control border-radius |
--caat-font-primary | Form font family |
3. Authoring Fields
| Field | Type | Required | Notes |
|---|---|---|---|
| Form Action URL | Path/URL | Yes | Endpoint that receives the submitted data |
| Method | Dropdown (GET / POST) | Yes | HTTP method; defaults to POST |
| Form Title | Text | No | Rendered as <h4> inside the form |
| Success Message | Rich text | No | Displayed on successful submission |
| Enable Multi-Step | Toggle | No | Enables step-based layout with progress indicator |
| Fields (Multifield) | |||
| Field Type | Dropdown | Yes | text | email | tel | select | textarea | checkbox | radio | date | file |
| Label | Text | Yes | Visible label for the field |
| Placeholder | Text | No | Placeholder text for inputs/textarea |
| Required | Toggle | No | Marks field as required |
| Validation Pattern | Text (regex) | No | Custom regex pattern for validation |
| Help Text | Text | No | Descriptive text below the input |
| Options List | Multifield (label/value) | Conditional | Required for select / radio field types |
| Step Group | Number | Conditional | Which step this field belongs to (multi-step only) |
4. Validation Rules
| Rule | Behaviour |
|---|---|
| Action URL must be populated | Dialog prevents save without a valid form action |
| At least one field must exist | Form is not rendered if the fields multifield is empty |
| Options required for select/radio | Dialog validation error when field type is select or radio and options list is empty |
| Email fields validate format | Client-side regex + type="email" ensures valid email format |
| Tel fields validate format | Pattern attribute enforces phone number format |
| Required fields must have content | Client-side required attribute + aria-required |
| Multi-step validates per step | Each step must be valid before progressing; focus moves to first error |
Behaviour contract (JS)
The design system ships no forms JavaScript — behaviour below is the implementing team's responsibility (AEM clientlib). The live demo includes a minimal reference implementation (goToStep() in this page's source) for the step-switching mechanics only.
| Behaviour | Required implementation |
|---|---|
| Step switching | Steps are sibling <fieldset class="caat-form__step"> elements; exactly one visible at a time (others .d-none). On change: update progress <li> classes (.completed for past, .active + aria-current="step" for current), then move focus to the first field of the revealed step. |
| Advance gating | Before advancing, run constraint validation on the current step only (fieldset.querySelectorAll(':invalid') or equivalent). If invalid: stay on step, render errors, focus the first invalid field. The demo's goToStep() deliberately omits gating — do not ship it as-is. |
| Back navigation | "Back" never validates — users may always return to a previous step with data preserved (steps are hidden, not unmounted). |
| Error rendering | Add .is-invalid to the control, set aria-invalid="true", insert/reveal the message element referenced by the control's aria-describedby, with role="alert". |
| Submit / pending | On submit: disable the submit button using the Button loading pattern (aria-busy="true", see Button contract §States) to prevent double submission; re-enable on failure with a role="alert" summary above the form. |
| Success | Replace the form with the authored success message (or redirect to the Thank-You page pattern); move focus to the success heading. |
5. Content Guidance
- Labels: Use clear, concise labels. Avoid jargon — say "Date of Birth" not "DOB."
- Placeholder text: Show an example value (e.g., "jane.smith@example.com") rather than repeating the label.
- Help text: Explain why the information is needed or the expected format (e.g., "Format: 000-000-000").
- Error messages: Be specific and actionable — "Please enter a valid email address" not "Invalid input."
- Submit button: Use a verb phrase that describes the action — "Submit Enrollment," "Send Message."
- Progress steps: Use short labels (2–3 words) for multi-step indicators.
- Privacy note: For forms collecting personal data, include a link to CAAT's Privacy Policy.
6. Semantic HTML
<form class="caat-form" action="/api/contact" method="POST" novalidate
aria-label="Contact Member Services"
data-analytics-component="form"
data-analytics-form-name="contact-member-services">
<div class="caat-form__group">
<label class="form-label" for="field-name">
Full Name <span class="text-danger" aria-hidden="true">*</span>
</label>
<input class="form-control" type="text" id="field-name"
name="name" required aria-required="true"
aria-describedby="field-name-help field-name-error">
<span class="caat-form__help" id="field-name-help">
As it appears on your pension statement.
</span>
<span class="caat-form__error" id="field-name-error" role="alert"
hidden>
Please enter your full name.
</span>
</div>
<!-- Grouped fields (radio/checkbox) -->
<fieldset class="caat-form__group">
<legend class="form-label">Preferred Contact Method</legend>
<label class="caat-form__radio">
<input type="radio" name="contactMethod" value="email"> Email
</label>
<label class="caat-form__radio">
<input type="radio" name="contactMethod" value="phone"> Phone
</label>
</fieldset>
<button type="submit" class="caat-button caat-button--primary">
Submit Inquiry
</button>
</form>
Multi-Step Structure
<form class="caat-form caat-form--multi-step" …>
<div class="caat-form__progress" aria-label="Form progress">
<ol class="caat-form__progress-steps">
<li class="caat-form__progress-step active" aria-current="step">
<span class="step-number">1</span> Personal Info
</li>
<li class="caat-form__progress-step">…</li>
</ol>
</div>
<fieldset class="caat-form__step" id="step-1">
<legend class="visually-hidden">Step 1: Personal Information</legend>
<!-- fields -->
</fieldset>
</form>
7. CSS Contract
| Class | Element | Notes |
|---|---|---|
.caat-form | Form root | Wrapper for all form variants |
.caat-form--multi-step | Form root modifier | Enables step-based layout |
.caat-form__group | Field container | Wraps label + input + help/error |
.caat-form__label | Label | Font-weight via token |
.caat-form__input | Text/email/tel/date/file inputs | Shared styling for single-line inputs |
.caat-form__select | Select dropdown | Custom arrow indicator |
.caat-form__textarea | Multi-line text | Resizable vertically only |
.caat-form__checkbox | Checkbox wrapper | Custom checkbox styling |
.caat-form__radio | Radio wrapper | Custom radio styling |
.caat-form__help | Help text | Below the input; uses help-color token |
.caat-form__error | Error message | Red text with icon; hidden until invalid |
.caat-form__progress | Progress container | Multi-step progress bar |
.caat-form__progress-steps | Ordered list of steps | Horizontal step indicators |
.caat-form__progress-step | Individual step | .active for current step |
.is-valid | State modifier | Green border for validated fields |
.is-invalid | State modifier | Red border + shows error message |
8. Accessibility
| Requirement | Implementation |
|---|---|
| Labels linked to inputs | Every <input> / <select> / <textarea> has a matching <label for="…"> |
| Help & error descriptions | aria-describedby references both help text and error message IDs |
| Required indicator | aria-required="true" on required fields; visual asterisk is aria-hidden |
| Invalid state | aria-invalid="true" set dynamically when validation fails |
| Error announcements | Error messages use role="alert" so screen readers announce immediately |
| Focus management | On validation failure, focus moves to first invalid field; on step change, focus moves to first field in new step |
| Grouped controls | Radio groups and checkbox groups wrapped in <fieldset> with <legend> |
| Progress indicator | aria-current="step" on active step; aria-label on progress container |
| Colour contrast | All form text, borders, and error states meet WCAG 2.1 AA (4.5:1 for text) |
| Touch targets | Minimum 44×44px tap area for checkboxes, radios, and buttons |
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 Implementation Notes
- Resource type:
caat/components/form - Proxy:
/apps/caat/components/form - Sling model: Provides field list, step configuration, action URL, and method.
- Client-side validation: Progressive enhancement — uses
novalidateon form, JS handles validation and focus management. - Server-side handling: Action URL should point to a Sling POST servlet or external API endpoint.
- Multi-step JS: A lightweight controller manages step visibility, progress state, and per-step validation.
- File uploads: When file field type is used, form encoding switches to
multipart/form-data. - CSRF protection: Include AEM CSRF token as hidden field for POST submissions.
- Captcha: Optional reCAPTCHA/hCaptcha integration configured at the form level.
12. Dialog 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).
{
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "cq/gui/components/authoring/dialog",
"content": {
"items": {
"tabs": {
"items": {
"formSettings": {
"jcr:title": "Form Settings",
"items": {
"columns": {
"items": {
"column": {
"items": {
"formTitle": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./formTitle", "fieldLabel": "Form Title" },
"actionUrl": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./actionUrl", "fieldLabel": "Action URL", "required": true },
"method": { "sling:resourceType": "granite/ui/components/coral/foundation/form/select", "name": "./method", "fieldLabel": "Method", "items": { "post": { "text": "POST", "value": "POST", "selected": true }, "get": { "text": "GET", "value": "GET" } } },
"enableMultiStep": { "sling:resourceType": "granite/ui/components/coral/foundation/form/checkbox", "name": "./enableMultiStep", "text": "Enable Multi-Step Layout" },
"successMessage": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textarea", "name": "./successMessage", "fieldLabel": "Success Message" }
}
}
}
}
}
},
"fields": {
"jcr:title": "Form Fields",
"items": {
"columns": {
"items": {
"column": {
"items": {
"fields": {
"sling:resourceType": "granite/ui/components/coral/foundation/form/multifield",
"fieldLabel": "Fields",
"composite": true,
"field": {
"items": {
"fieldType": { "sling:resourceType": "granite/ui/components/coral/foundation/form/select", "name": "./fieldType", "fieldLabel": "Field Type", "items": { "text": { "text": "Text", "value": "text" }, "email": { "text": "Email", "value": "email" }, "tel": { "text": "Telephone", "value": "tel" }, "select": { "text": "Dropdown", "value": "select" }, "textarea": { "text": "Textarea", "value": "textarea" }, "checkbox": { "text": "Checkbox", "value": "checkbox" }, "radio": { "text": "Radio Buttons", "value": "radio" }, "date": { "text": "Date", "value": "date" }, "file": { "text": "File Upload", "value": "file" } } },
"label": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./label", "fieldLabel": "Label", "required": true },
"placeholder": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./placeholder", "fieldLabel": "Placeholder" },
"required": { "sling:resourceType": "granite/ui/components/coral/foundation/form/checkbox", "name": "./required", "text": "Required" },
"validationPattern": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./validationPattern", "fieldLabel": "Validation Pattern (regex)" },
"helpText": { "sling:resourceType": "granite/ui/components/coral/foundation/form/textfield", "name": "./helpText", "fieldLabel": "Help Text" },
"options": { "sling:resourceType": "granite/ui/components/coral/foundation/form/multifield", "name": "./options", "fieldLabel": "Options (for select/radio)", "composite": true }
}
}
}
}
}
}
}
}
}
}
}
}
}
}
13. QA Acceptance Checklist
- All field types render correctly (text, email, tel, select, textarea, checkbox, radio, date, file).
- Required fields show asterisk and
aria-required="true". - Validation triggers on submit (not on blur for first interaction).
- Invalid fields show error message with
role="alert"and receive focus. - Valid fields show green border confirmation.
- Multi-step progress indicator updates correctly and announces step changes.
- Each step validates before allowing progression.
- Back button in multi-step preserves entered data.
- Labels are correctly associated (
for/idmatching). -
aria-describedbylinks help text and error messages to their fields. - Radio/checkbox groups use
fieldsetandlegend. - Form is fully operable via keyboard only.
- Analytics events fire: form-start, form-step, form-submit, form-error.
- Responsive layout renders correctly on mobile, tablet, and desktop.
- File upload field accepts specified file types and shows file name.
14. Definition of Done
| Area | Done means |
|---|---|
| Design | Figma component matches this contract. All field types, validation states, and multi-step progress are documented. Tokens are used — no hard-coded colour values. |
| Development | AEM proxy component created. HTL template emits semantic HTML per Section 6. CSS uses design tokens. JS handles validation, focus management, multi-step navigation, and analytics events. 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 (NVDA, VoiceOver). |
| Launch | Component deployed to AEM production. Content authors trained on multifield configuration. Documentation published. Analytics dashboards confirmed receiving all form events. |