Component Contract

Dropdown

Toggleable contextual menu for actions, filters, or secondary navigation within CAAT digital properties.

Live Demo

Standard Dropdown

Rendered

Split Button Dropdown

Rendered

Headers, Dividers & Disabled Items

Rendered

Directional Variants

Rendered

Dark Dropdown

Rendered

Open Menu

The menu panels shown statically (with .show) so the menu anatomy — headers, items, dividers, disabled — is visible without interaction.

States

Follows the Interaction States matrix. Menu items implement default, hover, focus, active (selected), and disabled. The static is-* classes mirror the live pseudo-class states.

Code sample

HTML

<!-- Standard dropdown -->
<div class="caat-dropdown dropdown" data-analytics-component="dropdown">
  <button class="caat-button caat-button--primary dropdown-toggle"
          type="button" data-bs-toggle="dropdown"
          aria-expanded="false" aria-haspopup="true">
    Trigger Label
  </button>
  <ul class="dropdown-menu caat-dropdown__menu">
    <li><a class="dropdown-item caat-dropdown__item" href="#"
           data-analytics-label="dropdown-select">Item One</a></li>
    <li><a class="dropdown-item caat-dropdown__item" href="#"
           data-analytics-label="dropdown-select">Item Two</a></li>
  </ul>
</div>

<!-- Split button dropdown -->
<div class="caat-dropdown btn-group" data-analytics-component="dropdown">
  <button type="button" class="caat-button caat-button--primary">
    Primary Action
  </button>
  <button type="button"
          class="caat-button caat-button--primary dropdown-toggle dropdown-toggle-split"
          data-bs-toggle="dropdown" aria-expanded="false" aria-haspopup="true">
    <span class="visually-hidden">Toggle Dropdown</span>
  </button>
  <ul class="dropdown-menu caat-dropdown__menu">
    <li><a class="dropdown-item caat-dropdown__item" href="#">Alt Action</a></li>
  </ul>
</div>

<!-- Directional: .dropup, .dropend, .dropstart -->
<div class="caat-dropdown dropup">…</div>

<!-- Dark menu: add .dropdown-menu-dark -->
<ul class="dropdown-menu dropdown-menu-dark caat-dropdown__menu">…</ul>

<!-- Headers, dividers, disabled items -->
<li><h6 class="dropdown-header">Section Header</h6></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item disabled" aria-disabled="true">Locked</a></li>

1. Component Summary

Component nameDropdown
AEM component namecaat/components/dropdown
Recommended implementationCustom AEM component wrapping Bootstrap 5 dropdown
Component groupCAAT Design System
PurposeToggleable contextual menu for actions, filters, or secondary navigation
StatusNew

Primary use cases

  1. Action menus — contextual actions on a record or section (e.g. Download, Print, Share)
  2. Filter menus — quick filter selections for tables or lists
  3. Secondary navigation — overflow or auxiliary page links
  4. Split buttons — primary action with alternative options

Non-goals

  1. Not for form field selection — use the native <select> or Forms component instead
  2. Not for primary navigation — use the Navigation component
  3. Not for mega menus or multi-level nesting
  4. Not for tooltips or popovers — use dedicated tooltip components

2. Design System Source / Token Dependencies

TokenValue (reference)Purpose
--caat-blue-900#003750Menu item text colour on hover
--caat-blue-700#0b5a80Primary trigger button background
--caat-blue-100#e7f4fbActive/selected item background
--caat-grey-200#dfe6efDivider colour, item hover background
--caat-grey-100#e8ecf1Menu background
--caat-focus0 0 0 .25rem rgba(47,149,210,.35)Focus ring on trigger button and menu items
--caat-font-primary"Libre Franklin", Arial, sans-serifMenu typeface
--caat-shadow-sm0 .125rem .25rem rgba(0,0,0,.075)Menu elevation shadow

3. Authoring Fields

FieldTypeRequiredDescription
Trigger labelTextfieldYesText displayed on the dropdown trigger button
Trigger styleDropdownYesButton variant: primary, outline-primary
Dropdown typeDropdownYesStandard button or split button
DirectionDropdownNoMenu direction: dropdown (default), dropup, dropstart, dropend
Dark menuToggle/SwitchNoEnables .dropdown-menu-dark variant
ItemsMultifieldYesMenu items — each item has the fields below

Items multifield

FieldTypeRequiredDescription
Item typeSelectYesOne of: link, button, header, divider
LabelTextfieldYes (except divider)Visible text for the menu item or header
URLPath / URLConditionalRequired when item type is link
DisabledToggle/SwitchNoRenders the item as non-interactive with .disabled and aria-disabled="true"
IconTextfieldNoBootstrap Icon class (e.g. bi-eye) prepended to the label

4. Validation Rules

FieldRule
Trigger labelRequired, max 60 characters
ItemsAt least one item of type link or button required
Item labelRequired for link, button, and header types; max 80 characters
URLRequired when item type is link; must be a valid internal path or external URL
DirectionMust be one of: dropdown, dropup, dropstart, dropend

5. Content Guidance

Do

  • Use concise, action-oriented labels for menu items (e.g. "Download PDF", "Edit Profile")
  • Group related items using headers and dividers
  • Use split buttons when there is a clear primary action with secondary alternatives
  • Limit menus to 7–10 items maximum for scannability
  • Use icons sparingly to aid recognition, not decoration

Don't

  • Don't use dropdowns for form field selection — use <select> instead
  • Don't nest dropdowns within dropdowns
  • Don't use vague labels like "More" or "Options" without context
  • Don't put critical, frequently-used actions only inside a dropdown — make them visible
  • Don't mix navigation links and destructive actions in the same menu without visual separation

6. Semantic HTML

<!-- Standard dropdown -->
<div class="caat-dropdown ${directionClass}"
     data-analytics-component="dropdown">
  <button class="caat-button caat-button--${triggerVariant} dropdown-toggle"
          type="button" data-bs-toggle="dropdown"
          aria-expanded="false" aria-haspopup="true">
    ${triggerLabel}
  </button>
  <ul class="dropdown-menu ${darkClass} caat-dropdown__menu">
    <sly data-sly-list="${items}">
      <!-- header -->
      <li data-sly-test="${item.type == 'header'}">
        <h6 class="dropdown-header">${item.label}</h6>
      </li>
      <!-- divider -->
      <li data-sly-test="${item.type == 'divider'}">
        <hr class="dropdown-divider">
      </li>
      <!-- link -->
      <li data-sly-test="${item.type == 'link'}">
        <a class="dropdown-item caat-dropdown__item ${item.disabled ? 'disabled' : ''}"
           href="${item.url}" aria-disabled="${item.disabled}"
           data-analytics-label="dropdown-select">
          <i class="bi ${item.icon} me-2"
             data-sly-test="${item.icon}" aria-hidden="true"></i>
          ${item.label}
        </a>
      </li>
      <!-- button -->
      <li data-sly-test="${item.type == 'button'}">
        <button class="dropdown-item caat-dropdown__item ${item.disabled ? 'disabled' : ''}"
                type="button" ${item.disabled ? 'disabled' : ''}
                data-analytics-label="dropdown-select">
          <i class="bi ${item.icon} me-2"
             data-sly-test="${item.icon}" aria-hidden="true"></i>
          ${item.label}
        </button>
      </li>
    </sly>
  </ul>
</div>

<!-- Split button variant -->
<div class="caat-dropdown btn-group ${directionClass}"
     data-analytics-component="dropdown">
  <button type="button"
          class="caat-button caat-button--${triggerVariant}">
    ${triggerLabel}
  </button>
  <button type="button"
          class="caat-button caat-button--${triggerVariant} dropdown-toggle dropdown-toggle-split"
          data-bs-toggle="dropdown" aria-expanded="false" aria-haspopup="true">
    <span class="visually-hidden">Toggle Dropdown</span>
  </button>
  <ul class="dropdown-menu ${darkClass} caat-dropdown__menu">
    …items…
  </ul>
</div>

7. CSS Contract

ClassElementPurpose
.caat-dropdownRoot wrapperWraps the Bootstrap .dropdown (or .dropup, .dropend, .dropstart); applies CAAT token overrides
.caat-dropdown__menuMenu panelExtends .dropdown-menu; applies CAAT font, shadow, border-radius, and background tokens
.caat-dropdown__itemMenu itemExtends .dropdown-item; applies CAAT hover, active, focus, and disabled styles using design tokens

CSS file

/assets/css/components/dropdown.css

All custom styles must use CAAT design tokens. No hard-coded colour values. The .caat-dropdown class wraps Bootstrap's dropdown classes to layer CAAT-specific visual overrides without breaking Bootstrap functionality.

8. Accessibility

RequirementImplementation
aria-haspopup="true"Applied to the trigger button to indicate it opens a menu
aria-expandedSet to "false" when closed, "true" when open; managed automatically by Bootstrap
Arrow-key navigation / move focus between menu items (Bootstrap built-in)
Escape to closePressing Escape closes the menu and returns focus to the trigger button (Bootstrap default)
Disabled itemsDisabled items use .disabled class and aria-disabled="true"; for <button> types, also include the disabled attribute
Focus managementWhen the menu opens, focus moves to the first non-disabled item. On close, focus returns to the trigger
Role semanticsBootstrap applies role="menu" and role="menuitem" automatically via JS for keyboard-navigated dropdowns

Keyboard interaction

  • Enter / Space — open the dropdown (on trigger) or activate the focused item
  • — open the dropdown (on trigger) or move focus to the next item
  • — move focus to the previous item
  • Escape — close the dropdown and return focus to the trigger
  • Tab — close the dropdown and move focus to the next focusable element on the page
  • Home / End — move focus to the first / last item in the menu

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/dropdown
  • Resource type: caat/components/dropdown
  • Component group: CAAT Design System
  • Sling Model: Create a Sling Model to expose trigger label, trigger variant, dropdown type, direction, dark mode, and items multifield.
  • Unique ID generation: Not required — Bootstrap manages dropdown state via data-bs-toggle without needing unique element IDs.
  • Client library: caat.components.dropdown — includes CSS and JS for analytics event wiring.
  • Policies: Configure allowed item types and maximum item count 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/dropdown/_cq_dialog/.content.xml (pseudo-YAML)
jcr:root:
  sling:resourceType: cq/gui/components/authoring/dialog
  jcr:title: Dropdown
  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 label
                required: true
                maxlength: 60
              triggerStyle:
                sling:resourceType: granite/ui/components/coral/foundation/form/select
                name: ./triggerStyle
                fieldLabel: Trigger button style
                required: true
                items:
                  - { text: Primary, value: primary }
                  - { text: Outline Primary, value: outline-primary }
              dropdownType:
                sling:resourceType: granite/ui/components/coral/foundation/form/select
                name: ./dropdownType
                fieldLabel: Dropdown type
                required: true
                items:
                  - { text: Standard, value: standard }
                  - { text: Split Button, value: split }
              items:
                sling:resourceType: granite/ui/components/coral/foundation/form/multifield
                name: ./items
                fieldLabel: Menu items
                composite: true
                items:
                  field:
                    sling:resourceType: granite/ui/components/coral/foundation/container
                    items:
                      itemType:
                        sling:resourceType: granite/ui/components/coral/foundation/form/select
                        name: ./itemType
                        fieldLabel: Item type
                        required: true
                        items:
                          - { text: Link, value: link }
                          - { text: Button, value: button }
                          - { text: Header, value: header }
                          - { text: Divider, value: divider }
                      label:
                        sling:resourceType: granite/ui/components/coral/foundation/form/textfield
                        name: ./label
                        fieldLabel: Label
                        maxlength: 80
                      url:
                        sling:resourceType: granite/ui/components/coral/foundation/form/pathfield
                        name: ./url
                        fieldLabel: URL
                      disabled:
                        sling:resourceType: granite/ui/components/coral/foundation/form/switch
                        name: ./disabled
                        fieldLabel: Disabled
                        checked: false
                      icon:
                        sling:resourceType: granite/ui/components/coral/foundation/form/textfield
                        name: ./icon
                        fieldLabel: Icon class (e.g. bi-eye)

          # ── Style tab ────────────────────────────────
          style:
            jcr:title: Style
            items:
              direction:
                sling:resourceType: granite/ui/components/coral/foundation/form/select
                name: ./direction
                fieldLabel: Direction
                items:
                  - { text: Down (default), value: dropdown }
                  - { text: Up, value: dropup }
                  - { text: End, value: dropend }
                  - { text: Start, value: dropstart }
                defaultValue: dropdown
              darkMenu:
                sling:resourceType: granite/ui/components/coral/foundation/form/switch
                name: ./darkMenu
                fieldLabel: Dark menu
                checked: false

          # ── Behaviour tab ────────────────────────────
          behaviour:
            jcr:title: Behaviour
            items:
              analyticsTracking:
                sling:resourceType: granite/ui/components/coral/foundation/form/switch
                name: ./analyticsTracking
                fieldLabel: Analytics tracking
                checked: true

13. QA Acceptance Checklist

  • Standard dropdown opens and closes on trigger click.
  • Split button dropdown renders primary action button and separate toggle.
  • Headers, dividers, and disabled items render correctly and disabled items are non-interactive.
  • Directional variants (dropup, dropend, dropstart) position the menu correctly.
  • Dark dropdown variant applies dark background and light text.
  • aria-expanded toggles between true and false on open/close.
  • aria-haspopup="true" is present on the trigger button.
  • / arrow keys navigate menu items.
  • Escape closes the menu and returns focus to the trigger.
  • Screen reader announces trigger as a popup button and reads menu items.
  • Analytics events (dropdown-open, dropdown-select) fire correctly with item labels.
  • Icons render correctly when configured, with aria-hidden="true".
  • Cross-browser tested: Chrome, Firefox, Safari, Edge.
  • Responsive: dropdown is usable on mobile viewports without overflow.

14. Definition of Done

AreaDone means
Design Figma component matches this contract. All variants (standard, split, directional, dark) are documented. Tokens are used — no hard-coded colour values.
Development AEM component created at /apps/caat/components/dropdown. HTL template emits semantic HTML per Section 6. CSS uses design tokens per Section 7. JS wires Bootstrap dropdown 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). Keyboard navigation verified with screen reader.
Launch Component deployed to AEM production. Content authors trained on authoring dialog. Analytics dashboards confirmed receiving dropdown-open and dropdown-select events.