Repository navigation
Closed
Labels
Description
Hi,
i'm trying to inject a custom checkbox (don't show again) into the tour's footer using tour.on("show", handler), but the injected DOM is consistently being overwritten by Shepherd js right after the step is rendered. but (don't show again) only render in first step only, i want the (don't show again) always render in all step. I'am using vue.js by the waay
here's my code
injectDismissCheckbox() {
const tryInject = (attempt = 0) => {
const footer = document.querySelector(".shepherd-footer");
console.log("footer:", footer);
if (!footer) {
console.log("Footer not found yet, retrying...", attempt);
if (attempt < 10) {
setTimeout(() => tryInject(attempt + 1), 50);
}
return;
}
const shepherdButtons = footer.querySelectorAll(".shepherd-button");
console.log("Shepherd buttons found (initial):", shepherdButtons);
setTimeout(() => {
const buttons = footer.querySelectorAll(".shepherd-button");
console.log("Shepherd buttons found (after delay):", buttons);
const existingCheckbox = document.getElementById("dontShowAgainCheckbox");
if (existingCheckbox) {
console.log("Removing existing checkbox");
existingCheckbox.closest("label")?.remove();
}
const wrapper = document.createElement("div");
wrapper.style.display = "flex";
wrapper.style.justifyContent = "space-between";
wrapper.style.alignItems = "center";
wrapper.style.width = "100%";
const checkboxWrapper = document.createElement("label");
checkboxWrapper.style.display = "flex";
checkboxWrapper.style.alignItems = "center";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = "dontShowAgainCheckbox";
checkbox.style.marginRight = "6px";
checkbox.checked = localStorage.getItem("dashboard_tour_dismissed") === "true";
const label = document.createElement("span");
label.textContent = "Don't show again";
checkboxWrapper.appendChild(checkbox);
checkboxWrapper.appendChild(label);
const btnGroup = document.createElement("div");
btnGroup.style.display = "flex";
btnGroup.style.gap = "8px";
buttons.forEach((btn) => btnGroup.appendChild(btn));
footer.innerHTML = "";
wrapper.appendChild(checkboxWrapper);
wrapper.appendChild(btnGroup);
footer.appendChild(wrapper);
console.log("Checkbox inserted into DOM:", checkboxWrapper);
console.log("Wrapper inserted into DOM:", wrapper);
}, 100);
};
tryInject();
} tour.addStep({
id: "step-6",
title: "Step 6: Ready to Share?",
text: "Your deck will be saved as a link, so it’s easy to share it via email, SMS and WhatsApp.",
attachTo: { element: ".shepherd-done", on: "top" },
buttons: [
{
text: "Back",
action: tour.back,
},
{
text: "Selesai",
action: () => {
const checkbox = document.getElementById(
"dontShowAgainCheckbox"
);
if (checkbox?.checked) {
localStorage.setItem(
"dashboard_tour_dismissed",
"true"
);
}
tour.complete();
this.launchAiPop();
},
},
],
});
// tour.on("show", this.injectDismissCheckbox);
tour.on("show", (event) => {
console.log("Step shown:", event.step.id);
this.injectDismissCheckbox(event.step);
});
tour.start();
},
},




Activity
chuckcarpenter commentedon Aug 13, 2026
Your injection is landing on a footer Shepherd is about to throw away — and, on steps after the first, on the wrong footer. Two separate things (line numbers below are against
main):1. The step element is rebuilt from scratch on every show.
_show()calls_setupElements(), which tears down the oldeland recreates it (shepherd.js/src/step.ts:768-774,747-761), andcreateShepherdFooterbuilds a brand-new<footer class="shepherd-footer">each time (shepherd.js/src/components/shepherd-footer.ts:6-16). A one-time injection can't survive a step change — the node is gone, not overwritten.2.
tour.on('show', …)fires before that element exists._showStep()triggers the tour'sshowand then callsstep.show()(shepherd.js/src/tour.ts:408-413). So at the moment your handler runs, the incoming step has no DOM at all. Outgoing steps are only hidden, never removed (step.ts:510-522), so your globaldocument.querySelector('.shepherd-footer')matches an earlier step's footer — still attached, justhidden— and you decorate a doomed node.Use the step-level
when.showinstead. It fires after the DOM is built (step.ts:813), is bound withthis= the step (step.ts:731-735), and runs for every step when declared ondefaultStepOptions:That deletes a lot of your code: no retry loop, no
setTimeout, nofooter.innerHTML = '', no re-parenting the buttons. And since the checkbox writes straight tolocalStorage, your "Selesai" action no longer needs to query the DOM at all.Three caveats — the first one matters for your Vue setup specifically:
when.show,thisis the Step, not your Vue component. Keep it a method/function(an arrow function would capture the component instead) and close over any component state you need, e.g.const vm = this;outside the tour config.whenreplacesdefaultStepOptions.whenwholesale — the merge is a shallowObject.assign(step.ts:715-722). Keep this hook in one place, or re-declare it on any step that needs its ownwhen.buttons: [{ text: "Don't show again", action() { … }, attrs: { 'data-test': 'dismiss' } }].attrslanded in 15.x (not in 14.x); latest published release is 15.2.3.The cookbook already shows this pattern for progress indicators — we never spelled out why it has to be
when.show, which is on us — tracked now in #3479. You waited far too long for an answer here.One honest caveat: this is from reading the source, not from reproducing your Vue setup — but the rebuild-per-show path and the event ordering are unambiguous in the code.