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
TypeDescriptionUse caseBenefit
PopupCentered overlay with backdropPromotions, newsletter signups, announcementsHigh visibility, user attention
FullscreenCovers entire viewportOnboarding flows, important announcementsMaximum impact, no distractions
SidebarSlides in from sideAdditional info, navigation, filtersNon-intrusive, contextual
InlineEmbedded in page contentProduct recommendations, related contentSeamless integration
OptionTypeDefaultDescription
idstringRequiredUnique identifier for the modal. Use descriptive names (e.g., welcome-modal, exit-intent-promo) for easier debugging and analytics tracking.
templatestringRequiredTemplate name fetched from modal_url. Follow naming convention: <template>-<lang>.txt (e.g., welcome-popup-en.txt). Templates support {{ }} macros for dynamic content.
groupstringOptionalGroup 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.
triggersarrayRequiredDefines 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.
filtersarrayOptionalDefines 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.
optionsobjectOptionalModal behavior and appearance options. Controls animations, close behavior, limits, theming, and accessibility features.
OptionTypeDefaultDescription
animationstring"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_instringanimationOverrides entry animation independently of exit. Useful when you need asymmetry (e.g., zoom-in in, fade-out out).
animation_outstringAuto-mappedExit animation. Auto-derives from animation (e.g., fade-infade-out). Set explicitly to fine‑tune dismissal feel (e.g., slide-up-out).
close_clickbooleantrueAllows dismiss on overlay click. Keep true for informational/optional UX; set false when action is critical or you must gate with explicit CTA.
close_escbooleantrueEnables Escape key to close. Recommended for accessibility and power users unless a forced action is required.
close_ctabooleanfalseWhen 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_autonumber0Auto-dismiss after N ms. Great for toast-like confirmations or low‑importance promos. Avoid for forms or long reads. 0 disables auto-close.
delaynumber0Wait time before first render (ms). Use to avoid layout jank on load, or to await context (e.g., price fetch). Typical: 1000–3000.
themestring"auto"Visual theme. auto respects user/system preference; light/dark force styling. Pair with accessible contrast in templates.
limit_typestring"session"Frequency scope. session resets on new tab/session; local persists in localStorage across visits. Choose local for campaign caps.
limit_countnumber1Max 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

OptionTypeDefaultDescription
idstringRequiredUnique identifier for the A/B test. Use descriptive names (e.g., newsletter-signup-ab, promo-banner-test) for easier analytics tracking and debugging.
persistbooleanfalseStore chosen variant in localStorage for consistency across sessions. Enable for user experience continuity; disable for true randomization on each visit.
variantstringnullForce 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_keystringAuto-generatedlocalStorage key for persisting variant choice. Auto-generated as ab_${modalId}. Override only if you need custom storage logic or conflict resolution.
variationsarrayRequiredArray 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 table

OptionTypeDefaultDescription
mlarray[]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.
slbooleanfalseSkip 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.
dlstring"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

User Languageml Listsl SettingResult
"es"["en", "es", "fr"]falseShows Spanish template
"de"["en", "es", "fr"]falseShows English template (first in ml)
"de"["en", "es", "fr"]trueModal skipped (language not allowed)
"en"[]falseShows English template (default)

Close behavior options

Close behavior table

OptionDescriptionExpected behavior
close_click: trueClose on overlay clickClicking outside modal closes it
close_esc: trueClose on Escape keyPressing Escape closes modal
close_cta: trueOnly CTA can closeModal only closes when CTA button is clicked
close_auto: 5000Auto-close after 5 secondsModal 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

TypeScopeDescription
sessionBrowser sessionResets when user closes browser/tab
locallocalStoragePersists 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

ActionDescriptionHTML Required
pushShowTriggers native push notification prompt<button id="pushShow">Enable notifications</button>
pushHidePlaceholder action (no-op)<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.