Modals
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
Modal types
- 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
Modal configuration
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 frommodal_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.
Modal options
animation(string, default:"fade-in"): Entry animation for perceived quality and attention. Useslide-*for directional context (sidebars),zoom-infor hero promos,bounce-insparingly for playful UIs.animation_in(string, default:animation): Overrides entry animation independently of exit. Useful when you need asymmetry (e.g.,zoom-inin,fade-outout).animation_out(string, default: Auto-mapped): Exit animation. Auto-derives fromanimation(e.g.,fade-in→fade-out). Set explicitly to fine‑tune dismissal feel (e.g.,slide-up-out).close_click(boolean, default:true): Allows dismiss on overlay click. Keeptruefor informational/optional UX; setfalsewhen 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): Whentrue, 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.0disables 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.autorespects user/system preference;light/darkforce styling. Pair with accessible contrast in templates.limit_type(string, default:"session"): Frequency scope.sessionresets on new tab/session;localpersists in localStorage across visits. Chooselocalfor campaign caps.limit_count(number, default:1): Max shows per scope. Typical values: 1 (once), 3 (gentle reminder). Works withgroupto 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.) ornullfor auto-selection. Use for debugging specific variants or gradual rollouts. Leavenullfor production A/B tests.storage_key(string, default: Auto-generated): localStorage key for persisting variant choice. Auto-generated asab_${modalId}. Override only if you need custom storage logic or conflict resolution.variations(array, required): Array of variant objects withname,weight, andmodal. 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:
- User-set language - Set via
Hood('setuserlanguage', 'en') - Browser language - Detected from
navigator.language - Modal default language (
dl) - Fallback language specified in modal config - 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 inmllist. Whentrue, acts as a strict filter - modal won’t show if user’s language isn’t supported. Whenfalse, falls back todlor first language inml.dl(string, default:"en"): Default language fallback. Only used whensl: falseand user’s language isn’t inmllist. Whensl: true, this option is ignored and modal is skipped instead.
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.txtNjY4bL60/welcome-popup-es.txtNjY4bL60/welcome-popup-fr.txt
Language fallback behavior
Scenario 1: User language is supported
- User Language:
"es" mlList:["en", "es", "fr"]slSetting:false- Result: ✅ Shows Spanish template
Scenario 2: User language not supported, fallback enabled
- User Language:
"de" mlList:["en", "es", "fr"]slSetting:false- Result: ✅ Shows English template (first in
ml)
Scenario 3: User language not supported, skip enabled
- User Language:
"de" mlList:["en", "es", "fr"]slSetting:true- Result: ⏭️ Modal skipped (language not allowed)
Scenario 4: All languages allowed
- User Language:
"en" mlList:[]slSetting:false- Result: ✅ Shows English template (default)
Close behavior options
Close behavior
close_click: true: Close on overlay click. Clicking outside modal closes itclose_esc: true: Close on Escape key. Pressing Escape closes modalclose_cta: true: Only CTA can close. Modal only closes when CTA button is clickedclose_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/tablocal: 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 attributes —
role="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.