Skip to content

Custom Content in Footer Gets Overwritten on Step Change #3216

Closed

Description

@iqmalr

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();
        },
    },

Image

Image

Image

Image

Activity

  1. chuckcarpenter commented on Aug 13, 2026

    @chuckcarpenter
    Member

    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 old el and recreates it (shepherd.js/src/step.ts:768-774, 747-761), and createShepherdFooter builds 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's show and then calls step.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 global document.querySelector('.shepherd-footer') matches an earlier step's footer — still attached, just hidden — and you decorate a doomed node.

    Use the step-level when.show instead. It fires after the DOM is built (step.ts:813), is bound with this = the step (step.ts:731-735), and runs for every step when declared on defaultStepOptions:

    const tour = new Shepherd.Tour({
      defaultStepOptions: {
        when: {
          show() {
            const footer = this.getElement()?.querySelector('.shepherd-footer');
            if (!footer) return; // steps without `buttons` have no footer
    
            const label = document.createElement('label');
            label.className = 'shepherd-dont-show-again';
    
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked =
              localStorage.getItem('dashboard_tour_dismissed') === 'true';
            checkbox.addEventListener('change', () => {
              localStorage.setItem(
                'dashboard_tour_dismissed',
                String(checkbox.checked)
              );
            });
    
            label.append(checkbox, document.createTextNode(" Don't show again"));
            footer.prepend(label);
          }
        }
      }
    });
    .shepherd-footer { align-items: center; }
    .shepherd-dont-show-again {
      align-items: center;
      display: flex;
      font-size: 0.8rem;
      gap: 6px;
      margin-right: auto; /* footer is flex/flex-end, so this pushes it left */
    }

    That deletes a lot of your code: no retry loop, no setTimeout, no footer.innerHTML = '', no re-parenting the buttons. And since the checkbox writes straight to localStorage, your "Selesai" action no longer needs to query the DOM at all.

    Three caveats — the first one matters for your Vue setup specifically:

    • Inside when.show, this is 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.
    • A per-step when replaces defaultStepOptions.when wholesale — the merge is a shallow Object.assign (step.ts:715-722). Keep this hook in one place, or re-declare it on any step that needs its own when.
    • If a plain button would do instead of a checkbox, skip the DOM surgery entirely: buttons: [{ text: "Don't show again", action() { … }, attrs: { 'data-test': 'dismiss' } }]. attrs landed 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions