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
| Type | Description | Use case | Benefit |
|---|---|---|---|
| Popup | Centered overlay with backdrop | Promotions, newsletter signups, announcements | High visibility, user attention |
| Fullscreen | Covers entire viewport | Onboarding flows, important announcements | Maximum impact, no distractions |
| Sidebar | Slides in from side | Additional info, navigation, filters | Non-intrusive, contextual |
| Inline | Embedded in page content | Product recommendations, related content | Seamless integration |
Modal configuration
| Option | Type | Default | Description |
|---|---|---|---|
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. |
Modal options table
| Option | Type | Default | Description |
|---|---|---|---|
animation | string | "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 | animation | Overrides entry animation independently of exit. Useful when you need asymmetry (e.g., zoom-in in, fade-out out). |
animation_out | string | Auto-mapped | Exit animation. Auto-derives from animation (e.g., fade-in → fade-out). Set explicitly to fine‑tune dismissal feel (e.g., slide-up-out). |
close_click | boolean | 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 | true | Enables Escape key to close. Recommended for accessibility and power users unless a forced action is required. |
close_cta | boolean | 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 | 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 | 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 | "auto" | Visual theme. auto respects user/system preference; light/dark force styling. Pair with accessible contrast in templates. |
limit_type | string | "session" | Frequency scope. session resets on new tab/session; local persists in localStorage across visits. Choose local for campaign caps. |
limit_count | number | 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
| Option | Type | Default | Description |
|---|---|---|---|
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 | false | Store chosen variant in localStorage for consistency across sessions. Enable for user experience continuity; disable for true randomization on each visit. |
variant | string | 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 | 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:
- 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 table
| Option | Type | Default | Description |
|---|---|---|---|
ml | array | [] | 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 | 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 | "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. |
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
| User Language | ml List | sl Setting | Result |
|---|---|---|---|
"es" | ["en", "es", "fr"] | false | Shows Spanish template |
"de" | ["en", "es", "fr"] | false | Shows English template (first in ml) |
"de" | ["en", "es", "fr"] | true | Modal skipped (language not allowed) |
"en" | [] | false | Shows English template (default) |
Close behavior options
Close behavior table
| Option | Description | Expected 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
| Type | Scope | Description |
|---|---|---|
session | Browser session | Resets when user closes browser/tab |
local | localStorage | 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
| Action | Description | HTML Required |
|---|---|---|
pushShow | Triggers native push notification prompt | <button id="pushShow">Enable notifications</button> |
pushHide | Placeholder 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.