Modals

Complete guide to modal configuration, targeting, A/B testing, and accessibility.
developer

What are modals?

Modals are rule-driven overlays that appear on your website to engage users with targeted messages, offers, or calls-to-action. They’re rendered in Shadow DOM (with iframe fallback) and follow a simple flow:

trigger → filters → show modal

Benefits:

  • Targeted messaging — Show relevant content based on user behavior, location, or properties
  • A/B testing — Test different versions to optimize conversion
  • Multi-language support — Automatically serve content in user’s preferred language
  • Accessibility built-in — WCAG compliant with focus management and screen reader support
  • Flexible triggers — Page views, scroll, timer, clicks, form submissions, and more
  • Popup: Centered overlay with backdrop. Use case: Promotions, newsletter signups, announcements. Benefit: High visibility, user attention
  • Fullscreen: Covers entire viewport. Use case: Onboarding flows, important announcements. Benefit: Maximum impact, no distractions
  • Sidebar: Slides in from side. Use case: Additional info, navigation, filters. Benefit: Non-intrusive, contextual
  • Inline: Embedded in page content. Use case: Product recommendations, related content. Benefit: Seamless integration
  • id (string, required): Unique identifier for the modal. Use descriptive names (e.g., welcome-modal, exit-intent-promo) for easier debugging and analytics tracking.
  • template (string, required): Template name fetched from modal_url. Follow naming convention: <template>-<lang>.txt (e.g., welcome-popup-en.txt). Templates support {{ }} macros for dynamic content.
  • group (string, optional): Group name to prevent multiple modals from same group showing on one page. Use for related modals (e.g., "entry" for welcome/onboarding flows) or competing campaigns.
  • triggers (array, required): Defines when a modal is eligible to show (lifecycle, timing, user actions, element visibility). Triggers capture events, then the engine evaluates filters before rendering. See Events and triggers.
  • filters (array, optional): Defines who should see the modal using a rule engine (macros + operators). Filters evaluate user/session/url/context to include/exclude audiences with precise conditions. See Filtering.
  • options (object, optional): Modal behavior and appearance options. Controls animations, close behavior, limits, theming, and accessibility features.
  • animation (string, default: "fade-in"): Entry animation for perceived quality and attention. Use slide-* for directional context (sidebars), zoom-in for hero promos, bounce-in sparingly for playful UIs.
  • animation_in (string, default: animation): Overrides entry animation independently of exit. Useful when you need asymmetry (e.g., zoom-in in, fade-out out).
  • animation_out (string, default: Auto-mapped): Exit animation. Auto-derives from animation (e.g., fade-infade-out). Set explicitly to fine‑tune dismissal feel (e.g., slide-up-out).
  • close_click (boolean, default: true): Allows dismiss on overlay click. Keep true for informational/optional UX; set false when action is critical or you must gate with explicit CTA.
  • close_esc (boolean, default: true): Enables Escape key to close. Recommended for accessibility and power users unless a forced action is required.
  • close_cta (boolean, default: false): When true, only CTAs can close the modal (overlay/Escape ignored). Use for gated actions (e.g., permission prompts, compliance steps). Combine with visible secondary button for ethical UX.
  • close_auto (number, default: 0): Auto-dismiss after N ms. Great for toast-like confirmations or low‑importance promos. Avoid for forms or long reads. 0 disables auto-close.
  • delay (number, default: 0): Wait time before first render (ms). Use to avoid layout jank on load, or to await context (e.g., price fetch). Typical: 1000–3000.
  • theme (string, default: "auto"): Visual theme. auto respects user/system preference; light/dark force styling. Pair with accessible contrast in templates.
  • limit_type (string, default: "session"): Frequency scope. session resets on new tab/session; local persists in localStorage across visits. Choose local for campaign caps.
  • limit_count (number, default: 1): Max shows per scope. Typical values: 1 (once), 3 (gentle reminder). Works with group to avoid multiple modals competing.

Basic modal structure

{
  "id": "welcome-modal",
  "template": "welcome-popup",
  "group": "entry",
  "triggers": [{ "type": "pageView" }],
  "filters": [
    { "c": "{{data.segment}}", "o": "eq", "m": "prospect" }
  ],
  "options": {
    "animation": "fade-in",
    "close_click": true,
    "close_esc": true,
    "close_cta": false,
    "close_auto": 0,
    "limit_type": "session",
    "limit_count": 1,
    "theme": "auto",
    "delay": 0
  }
}

A/B testing

Modals support A/B testing to optimize conversion rates and user engagement. You can test different templates, animations, triggers, or complete modal configurations to find what works best for your audience.

Benefits:

  • Conversion optimization — Test different CTAs, messaging, or designs
  • User experience insights — Understand which variants perform better
  • Data-driven decisions — Make informed choices based on real user behavior
  • Consistent experience — Users see the same variant across sessions (when persist: true)

A/B test configuration

  • id (string, required): Unique identifier for the A/B test. Use descriptive names (e.g., newsletter-signup-ab, promo-banner-test) for easier analytics tracking and debugging.
  • persist (boolean, default: false): Store chosen variant in localStorage for consistency across sessions. Enable for user experience continuity; disable for true randomization on each visit.
  • variant (string, default: null): Force specific variant ("A", "B", etc.) or null for auto-selection. Use for debugging specific variants or gradual rollouts. Leave null for production A/B tests.
  • storage_key (string, default: Auto-generated): localStorage key for persisting variant choice. Auto-generated as ab_${modalId}. Override only if you need custom storage logic or conflict resolution.
  • variations (array, required): Array of variant objects with name, weight, and modal. Each variant defines a complete modal configuration. Weight determines selection probability (equal weights = 50/50 split).

{
  "id": "promo-ab-test",
  "ab": {
    "id": "promo-ab-experiment",
    "persist": true,
    "variant": null,
    "storage_key": "ab_promo",
    "variations": [
      {
        "name": "A",
        "weight": 1,
        "modal": {
          "template": "promo-variant-a",
          "triggers": [{ "type": "pageView" }],
          "options": { "theme": "light" }
        }
      },
      {
        "name": "B",
        "weight": 1,
        "modal": {
          "template": "promo-variant-b",
          "triggers": [{ "type": "pageView" }],
          "options": { "theme": "dark" }
        }
      }
    ]
  }
}

A/B test example

{
  "id": "newsletter-signup",
  "ab": {
    "id": "newsletter-ab",
    "persist": true,
    "variations": [
      {
        "name": "Control",
        "weight": 1,
        "modal": {
          "template": "newsletter-control",
          "options": { "animation": "fade-in" }
        }
      },
      {
        "name": "Variant A",
        "weight": 1,
        "modal": {
          "template": "newsletter-variant-a",
          "options": { "animation": "slide-up" }
        }
      }
    ]
  }
}

Multi-language modals

Language detection logic

The SDK determines which language to use for modal templates using this priority:

  1. User-set language - Set via Hood('setuserlanguage', 'en')
  2. Browser language - Detected from navigator.language
  3. Modal default language (dl) - Fallback language specified in modal config
  4. English - Final fallback

Language options

  • ml (array, default: []): Array of allowed language codes (e.g., ["en", "es", "fr"]). When empty, all languages are allowed. Use to restrict modal to specific markets or when you only have templates for certain languages.
  • sl (boolean, default: false): Skip modal if user language not in ml list. When true, acts as a strict filter - modal won’t show if user’s language isn’t supported. When false, falls back to dl or first language in ml.
  • dl (string, default: "en"): Default language fallback. Only used when sl: false and user’s language isn’t in ml list. When sl: true, this option is ignored and modal is skipped instead.
Language filtering behavior

sl: true — Modal acts as a language filter. If user’s language isn’t in ml, modal is skipped entirely (ignores dl).

sl: false — Modal shows for all users. If user’s language isn’t in ml, falls back to dl or first language in ml.

Language configuration

{
  "id": "welcome-multilang",
  "template": "welcome-popup",
  "ml": ["en", "es", "fr", "de"],  // Allowed languages
  "sl": true,                     // Skip modal if user language not in ml
  "dl": "en"                      // Default language fallback
}

Template naming convention

Templates are fetched from modal_url using this pattern:

<tag-prefix>/<template>-<lang>.txt

Examples:

  • NjY4bL60/welcome-popup-en.txt
  • NjY4bL60/welcome-popup-es.txt
  • NjY4bL60/welcome-popup-fr.txt

Language fallback behavior

Scenario 1: User language is supported

  • User Language: "es"
  • ml List: ["en", "es", "fr"]
  • sl Setting: false
  • Result: ✅ Shows Spanish template

Scenario 2: User language not supported, fallback enabled

  • User Language: "de"
  • ml List: ["en", "es", "fr"]
  • sl Setting: false
  • Result: ✅ Shows English template (first in ml)

Scenario 3: User language not supported, skip enabled

  • User Language: "de"
  • ml List: ["en", "es", "fr"]
  • sl Setting: true
  • Result: ⏭️ Modal skipped (language not allowed)

Scenario 4: All languages allowed

  • User Language: "en"
  • ml List: []
  • sl Setting: false
  • Result: ✅ Shows English template (default)

Close behavior options

Close behavior

  • close_click: true: Close on overlay click. Clicking outside modal closes it
  • close_esc: true: Close on Escape key. Pressing Escape closes modal
  • close_cta: true: Only CTA can close. Modal only closes when CTA button is clicked
  • close_auto: 5000: Auto-close after 5 seconds. Modal automatically closes after specified time

Close behavior examples

// Standard modal - closes on click outside or Escape
{
  "options": {
    "close_click": true,
    "close_esc": true,
    "close_cta": false
  }
}

// CTA-only modal - only closes when user clicks CTA button
{
  "options": {
    "close_click": false,
    "close_esc": false,
    "close_cta": true
  }
}

// Auto-closing modal - closes automatically after 10 seconds
{
  "options": {
    "close_click": true,
    "close_esc": true,
    "close_auto": 10000
  }
}

Frequency and limits

Limit types

  • session: Browser session scope. Resets when user closes browser/tab
  • local: localStorage scope. Persists across browser sessions

Limit configuration

{
  "options": {
    "limit_type": "session",  // or "local"
    "limit_count": 3         // Show max 3 times per scope
  }
}

Grouping

Modals can be grouped to control display behavior and prevent conflicts. All modals and their triggers are registered simultaneously in the order they’re defined.

Group behavior:

  • With group: Only the first modal in a group that meets trigger/filter conditions will be shown
  • Without group: All modals without groups will be shown sequentially (one after another) as their triggers fire

Display order:

  • Modals are displayed one at a time, not simultaneously
  • When one modal closes, the next modal in the queue will be shown
  • Order depends on when triggers fire and filters are satisfied
{
  "id": "welcome-modal",
  "group": "entry",
  "options": { "limit_type": "session", "limit_count": 1 }
},
{
  "id": "exit-intent-modal", 
  "group": "entry",
  "options": { "limit_type": "session", "limit_count": 1 }
}

Only one modal from the “entry” group will show per page, regardless of triggers.

Accessibility

Built-in accessibility features

  • Focus management — Focus is trapped inside the modal
  • ARIA attributesrole="dialog", aria-modal="true"
  • Screen reader support — Optional aria-label, aria-labelledby, aria-describedby
  • Keyboard navigation — Escape key closes modal
  • High contrast — Supports system theme preferences

Accessibility configuration

{
  "options": {
    "theme": "auto",  // Respects user's system theme preference
    "close_esc": true // Ensures keyboard accessibility
  }
}

CTA actions

Built-in CTA actions

  • pushShow: Triggers native push notification prompt. HTML required: <button id="pushShow">Enable notifications</button>
  • pushHide: Placeholder action (no-op). HTML required: <button id="pushHide">No thanks</button>

CTA implementation

Template HTML:

<div class="modal-content">
  <h2>Stay updated!</h2>
  <p>Get notified about new features and updates.</p>
  
  <button id="pushShow" class="cta-button">
    Enable notifications
  </button>
  
  <button id="pushHide" class="secondary-button">
    No thanks
  </button>
</div>

Modal configuration:

{
  "id": "push-prompt",
  "template": "push-notification-prompt",
  "options": {
    "close_cta": true  // Only CTA buttons can close this modal
  }
}

Custom CTA actions

You can add custom CTA actions by extending the actionRef object in your SDK configuration:

// Custom CTA action
window.HoodEngage = window.HoodEngage || [];
function Hood() { HoodEngage.push(arguments); }

Hood('init', 'TAG_ID', {
  modals_config: {
    modals: [{
      id: 'custom-cta-modal',
      template: 'custom-cta-template',
      options: {
        close_cta: true
      }
    }]
  }
});

// Custom action handler
Hood('on', 'modalAction', (action, modalId) => {
  if (action === 'customAction') {
    // Handle custom action
    console.log('Custom action triggered for modal:', modalId);
  }
});

Template HTML for custom action:

<button id="customAction" class="cta-button">
  Custom Action
</button>

Examples

Basic welcome modal

{
  "id": "welcome",
  "template": "welcome-popup",
  "triggers": [{ "type": "pageView" }],
  "filters": [
    { "c": "{{data.segment}}", "o": "eq", "m": "prospect" }
  ],
  "options": {
    "animation": "fade-in",
    "close_click": true,
    "close_esc": true,
    "limit_type": "session",
    "limit_count": 1
  }
}

A/B tested promotional modal

{
  "id": "promo-ab",
  "ab": {
    "id": "promo-experiment",
    "persist": true,
    "variations": [
      {
        "name": "A",
        "weight": 1,
        "modal": {
          "template": "promo-variant-a",
          "triggers": [{ "type": "scroll", "config": { "threshold": 0.5 } }],
          "options": { "theme": "light", "animation": "slide-up" }
        }
      },
      {
        "name": "B", 
        "weight": 1,
        "modal": {
          "template": "promo-variant-b",
          "triggers": [{ "type": "scroll", "config": { "threshold": 0.5 } }],
          "options": { "theme": "dark", "animation": "zoom-in" }
        }
      }
    ]
  }
}

Multi-language exit intent modal

{
  "id": "exit-intent-multilang",
  "template": "exit-intent-popup",
  "triggers": [{ "type": "exitIntent" }],
  "ml": ["en", "es", "fr", "de"],
  "sl": false,
  "dl": "en",
  "options": {
    "animation": "slide-up",
    "close_click": true,
    "close_esc": true,
    "limit_type": "session",
    "limit_count": 1
  }
}

Why Shadow DOM with iframe fallback?

We prioritize Shadow DOM for rendering because it provides:

  • Isolation and safety: Modal CSS/JS are encapsulated and won’t leak into the page, nor will page styles break the modal.
  • Predictable theming: Internal class names and animations won’t collide with site CSS; high-contrast/dark-mode can be applied reliably.
  • Accessibility control: Focus trap, ARIA roles, keyboard handlers live in an isolated subtree without side effects.
  • Performance: No additional browsing context; lower overhead than iframes in modern browsers.

We fall back to an iframe when isolation must be absolute or Shadow DOM is not viable:

  • Hard CSS collisions: Site-wide resets or aggressive selectors still affect injected HTML; iframe guarantees complete sandboxing.
  • CSP or script execution constraints: If inline scripts/styles or event handlers are restricted, iframe can succeed where Shadow DOM doesn’t.
  • Browser/edge failures: If creating a shadow root or injecting styles fails (rare), iframe ensures the modal still renders consistently.

In short: Shadow DOM is the default for speed and clean integration; iframe is the safety net for maximum isolation when the host page environment is hostile.