Environment
- Theme: Divi 5.5.2 (latest stable at the time of writing)
- WordPress: 6.9.4
- PHP: 8.4.20
- Browser observed breaking: iOS Safari (iPhone, current iOS). Reported by the site's editor on a real device; reproduced through code review of the shipped bundle.
- Site URL (private staging, basic-auth available on request):
https://staging.tierheim-hannover.de — the Canvas popup containing the "Opening hours" content opens after 3 s on every page (auto-trigger on the global Theme-Builder header), and its close-X cannot be tapped on mobile. The same X works on desktop with a mouse click.
Summary
In Divi 5, an Interaction whose Trigger is set to "Click" never responds to taps on touch devices. The handler is registered as click only — there is no pointerup or touchend fallback — and the immediate preventDefault() + stopPropagation() inside the handler prevents the browser from cleanly synthesising a click after touchend on a generic <div> (which is what most Divi modules end up as on the front-end, including the Icon module used for popup close-buttons).
The visible effect on our site is that the Canvas popup with the site's opening-hours notice auto-opens on every page after 3 s, and its X close-button is unresponsive to taps. Users on phones cannot dismiss the overlay until they navigate to another page (where it opens again 3 s later).
This is generic to every Interaction with trigger:"click" — close-buttons, custom show/hide toggles, tab-like behaviour built with Interactions, etc. — not specific to Canvas popups.
Root cause (verified in shipped Divi 5.5.2 bundle)
wp-content/themes/Divi/includes/builder-5/visual-builder/build/script-library-interactions.js contains a switch (t.trigger) block that wires up listeners per trigger type. The "click" branch reads:
case "click":
e instanceof HTMLElement && (e.style.cursor = "pointer"),
S(e, "click", (t => {
t.target?.closest(".et-vb-ui") || (
t.preventDefault(),
t.stopPropagation(),
s(t.timeStamp)
)
}));
break;
S(target, eventName, handler) is a thin wrapper around target.addEventListener(eventName, handler). Only "click" is registered. The other branches in the same switch are either desktop-only by definition (mouseEnter, mouseExit) or use observers (viewportEnter, viewportExit, breakpointEnter, breakpointExit), so the missing-touch problem is unique to the click branch.
Why click alone is insufficient on iOS Safari:
- The interaction target is usually a
<div> (e.g. Icon module → et_pb_icon div), not a native <button> or <a>. iOS Safari emits a synthetic click event after touchend on generic divs only when the element looks clickable to the touch heuristic — usually because of cursor: pointer or role="button".
- The handler in the Divi bundle calls
preventDefault() + stopPropagation() on the click event itself, which is fine — but the wrapping bundle also installs addEventListener("click", ..., true) (capture) handlers higher up that interact unpredictably with iOS's "click synthesis from touch" logic, especially when combined with the page-level scroll-handling listeners that Divi's other bundles add.
- Empirically: the close-X has
cursor:pointer, is in the viewport, has pointer-events:auto, touch-action:auto, and is not occluded by any other element (verified with document.elementsFromPoint(centerX, centerY) — top of stack is the icon's own <span>). A real iPhone tap still does not dismiss the popup, while a synthetic el.click() from the Web Inspector console does (confirming the handler is wired up but never invoked from a tap).
Reproduction (3 minutes, no specific site required)
- Install Divi 5.5.2 (or any 5.x with the same interactions bundle).
- On any page, drop an Icon module and a Text module.
- Add a Divi 5 Interaction to the Icon: Trigger = Click, Effect = Toggle visibility, Target = the Text module.
- View the page on an iPhone (or any iOS device, Safari).
- Expected: tapping the icon toggles the text module.
- Actual: nothing happens. The icon has
cursor: pointer, looks clickable, but no Interaction fires on tap.
A desktop mouse click works as expected, which masks the bug during builder testing.
We also reproduced the same dead-tap behaviour on:
- Canvas popups (
et_pb_canvas posts) where a close-X icon uses trigger:"click" with toggleVisibility — this is exactly our production case.
- Standalone non-popup show/hide toggles. (i.e. it is not Canvas-specific.)
Suggested fix
Inside the case "click" branch of script-library-interactions.js, register a pointerup (or touchend) listener alongside the existing click listener, and de-duplicate when both fire in the same gesture:
case "click":
if (e instanceof HTMLElement) {
e.style.cursor = "pointer";
}
let lastSynthDispatch = 0;
const runEffect = (timestamp) => {
s(timestamp);
};
S(e, "click", (ev) => {
if (ev.target?.closest(".et-vb-ui")) {
return;
}
ev.preventDefault();
ev.stopPropagation();
runEffect(ev.timeStamp);
});
// Mobile / touch / pen fallback. On iOS Safari, click is not reliably
// synthesised after touchend on a `<div>` once preventDefault() has been
// called inside the click handler. pointerup runs in the same gesture
// for touch + pen + mouse and lets us recover the behaviour.
S(e, "pointerup", (ev) => {
if (ev.pointerType !== "touch" && ev.pointerType !== "pen") {
return; // mouse already worked via the click listener above.
}
if (ev.target?.closest(".et-vb-ui")) {
return;
}
// If a real click did fire in the same gesture (some mobile browsers
// do still emit one), skip — the click branch already handled it.
// Track via lastSynthDispatch + a microtask-deferred check.
const fireToken = performance.now();
lastSynthDispatch = fireToken;
queueMicrotask(() => {
if (lastSynthDispatch !== fireToken) {
return; // a later pointerup superseded us; bail.
}
runEffect(ev.timeStamp);
});
});
break;
The cheapest possible change (if minimising churn matters more than perfect de-duplication) is just:
S(e, "click", clickHandler);
S(e, "pointerup", (ev) => {
if (ev.pointerType === "touch" || ev.pointerType === "pen") {
clickHandler(ev);
}
});
— filtering on pointerType keeps the mouse path on the existing click listener (no behaviour change there), and only adds a tap/pen path.
Workaround in place (on my production site)
We ship a small mu-plugin (mu-plugins/30-tierheim-third-party-workarounds.php, section T9) that injects a footer inline script. It:
- Reads
window.diviElementInteractionsData after DOMContentLoaded.
- For every entry with
trigger === "click" and enabled !== false, finds all elements matching .et-interaction-trigger-{class} and binds a pointerup handler.
- The handler skips
pointerType === "mouse" (already works upstream).
- It dispatches a synthetic
new MouseEvent("click", { bubbles: true, cancelable: true }) on the element, which then runs through Divi's own click listener — so we don't have to duplicate Divi's effect logic.
- It uses a 350 ms idempotency window via a
data-th-touch-click-fired-at attribute to suppress a second firing when the browser does eventually emit a native click after pointerup.
- It re-binds on the
ETBuilderInteractionsUpdate event so newly rendered trigger nodes (e.g. from a Canvas replay) also receive the fallback.
This is a stop-gap. We will remove it as soon as Divi ships a touch-aware interactions runtime.
Why this is worth fixing on the Divi side
- Silent UX failure on mobile. Every site that has built any "click" Interaction (a popular feature surfaced prominently in the Visual Builder) is currently degrading on the largest share of real-world traffic — mobile. Site owners discover this only when an end-user complains.
- No public API change required. The fix is internal to one file (
script-library-interactions.js), one switch branch.
- The pattern is well-established.
pointerup + pointerType filtering is the canonical 2026-era way to bridge mouse + touch + pen with one code path. Divi 5 already targets evergreen browsers, so there is no compatibility cost.
- Easy to test. Any iOS/Android device with the repro above will demonstrate it. We are happy to verify a beta build on our staging environment.
Notes
- We have not exhaustively surveyed which other interaction effects (cookie-set, attribute-toggle, preset-replace, …) are affected on top of
toggleVisibility. The wiring problem is on the listener registration side — every effect that hangs off a trigger:"click" interaction is affected the same way.
- We are NOT shipping
touchend instead of pointerup in the workaround because pointerup cleanly handles pen + touch + mouse with one path, and is passive: true by default (no scroll-jank risk).
Environment
https://staging.tierheim-hannover.de— the Canvas popup containing the "Opening hours" content opens after 3 s on every page (auto-trigger on the global Theme-Builder header), and its close-X cannot be tapped on mobile. The same X works on desktop with a mouse click.Summary
In Divi 5, an Interaction whose Trigger is set to "Click" never responds to taps on touch devices. The handler is registered as
clickonly — there is nopointeruportouchendfallback — and the immediatepreventDefault()+stopPropagation()inside the handler prevents the browser from cleanly synthesising a click aftertouchendon a generic<div>(which is what most Divi modules end up as on the front-end, including the Icon module used for popup close-buttons).The visible effect on our site is that the Canvas popup with the site's opening-hours notice auto-opens on every page after 3 s, and its X close-button is unresponsive to taps. Users on phones cannot dismiss the overlay until they navigate to another page (where it opens again 3 s later).
This is generic to every Interaction with
trigger:"click"— close-buttons, custom show/hide toggles, tab-like behaviour built with Interactions, etc. — not specific to Canvas popups.Root cause (verified in shipped Divi 5.5.2 bundle)
wp-content/themes/Divi/includes/builder-5/visual-builder/build/script-library-interactions.jscontains aswitch (t.trigger)block that wires up listeners per trigger type. The"click"branch reads:S(target, eventName, handler)is a thin wrapper aroundtarget.addEventListener(eventName, handler). Only"click"is registered. The other branches in the same switch are either desktop-only by definition (mouseEnter,mouseExit) or use observers (viewportEnter,viewportExit,breakpointEnter,breakpointExit), so the missing-touch problem is unique to theclickbranch.Why
clickalone is insufficient on iOS Safari:<div>(e.g. Icon module →et_pb_icondiv), not a native<button>or<a>. iOS Safari emits a synthetic click event after touchend on generic divs only when the element looks clickable to the touch heuristic — usually because ofcursor: pointerorrole="button".preventDefault()+stopPropagation()on the click event itself, which is fine — but the wrapping bundle also installsaddEventListener("click", ..., true)(capture) handlers higher up that interact unpredictably with iOS's "click synthesis from touch" logic, especially when combined with the page-level scroll-handling listeners that Divi's other bundles add.cursor:pointer, is in the viewport, haspointer-events:auto,touch-action:auto, and is not occluded by any other element (verified withdocument.elementsFromPoint(centerX, centerY)— top of stack is the icon's own<span>). A real iPhone tap still does not dismiss the popup, while a syntheticel.click()from the Web Inspector console does (confirming the handler is wired up but never invoked from a tap).Reproduction (3 minutes, no specific site required)
cursor: pointer, looks clickable, but no Interaction fires on tap.A desktop mouse click works as expected, which masks the bug during builder testing.
We also reproduced the same dead-tap behaviour on:
et_pb_canvasposts) where a close-X icon usestrigger:"click"withtoggleVisibility— this is exactly our production case.Suggested fix
Inside the
case "click"branch ofscript-library-interactions.js, register apointerup(ortouchend) listener alongside the existingclicklistener, and de-duplicate when both fire in the same gesture:The cheapest possible change (if minimising churn matters more than perfect de-duplication) is just:
— filtering on
pointerTypekeeps the mouse path on the existingclicklistener (no behaviour change there), and only adds a tap/pen path.Workaround in place (on my production site)
We ship a small mu-plugin (
mu-plugins/30-tierheim-third-party-workarounds.php, section T9) that injects a footer inline script. It:window.diviElementInteractionsDataafter DOMContentLoaded.trigger === "click"andenabled !== false, finds all elements matching.et-interaction-trigger-{class}and binds apointeruphandler.pointerType === "mouse"(already works upstream).new MouseEvent("click", { bubbles: true, cancelable: true })on the element, which then runs through Divi's own click listener — so we don't have to duplicate Divi's effect logic.data-th-touch-click-fired-atattribute to suppress a second firing when the browser does eventually emit a native click after pointerup.ETBuilderInteractionsUpdateevent so newly rendered trigger nodes (e.g. from a Canvas replay) also receive the fallback.This is a stop-gap. We will remove it as soon as Divi ships a touch-aware interactions runtime.
Why this is worth fixing on the Divi side
script-library-interactions.js), one switch branch.pointerup+pointerTypefiltering is the canonical 2026-era way to bridge mouse + touch + pen with one code path. Divi 5 already targets evergreen browsers, so there is no compatibility cost.Notes
toggleVisibility. The wiring problem is on the listener registration side — every effect that hangs off atrigger:"click"interaction is affected the same way.touchendinstead ofpointerupin the workaround becausepointerupcleanly handles pen + touch + mouse with one path, and ispassive: trueby default (no scroll-jank risk).