Event fires twice on click on published page, not in Visual Editor

I have a custom component, an animated hamburger menu button. Here’s the code in React:

import { useState } from "react";

import styles from "./HamburgerButton.module.css";

export const HamburgerButton = ({
  color: backgroundColor,
}: {
  color: string;
}) => {
  const [checked, setChecked] = useState(false);
  const handleChange = () => setChecked(!checked);

  return (
    <>
      <input
        type="checkbox"
        id={styles.checkbox}
        checked={checked}
        onChange={handleChange}
      />
      <label className={styles.label} htmlFor={styles.checkbox}>
        <div style={{ backgroundColor }}></div>
        <div style={{ backgroundColor }}></div>
        <div style={{ backgroundColor }}></div>
      </label>
    </>
  );
};

I’ve placed my button along with a mobile menu inside my Builder page. Then I:

  1. Added a click handler to the hamburger menu button, which toggles the boolean state.isMobileMenuOpen
  2. Bound the mobile menu’s transform CSS property to depend on the value of state.isMobileMenuOpen: state.isMobileMenuOpen ? 'translateX(0)' : 'translateX(-100%)'. The idea is that the mobile menu will slide in from the left when open and slide back when it’s closed. Pretty standard.

Everything works great inside the Visual Editor. However, when I click on the hamburger menu button on my actual page, the binding callback seems to fire twice:

Since it’s toggling the value of isMobileMenuOpen, the result is that my menu never opens, because it goes from true to false very rapidly.

How do I fix it and what’s the root cause? And why does it work inside the Visual Editor but not on my actual page?

Builder content link

After a some experimentation, I narrowed down the problem.

The double trigger behavior occurs when there’s:

  1. an <input> in a custom component that’s not visible on the screen (e.g., display: none, opacity: 0, visibility: hidden), and
  2. a label associated with the input using the for HTML attribute.

This behavior occurs for any input type, not just checkboxes.

What appears to be happening is that the click event propagates to both the input and the label. I’m guessing that the label is what’s receiving the click event first, then something is causing the click to be passed onto the input, resulting in two click events back-to-back.

My workaround is to set disabled="true" on the checkbox, which seems to prevent the duplicate event from firing. This trick works for my use case because I don’t actually need to interact with the checkbox, it’s a CSS hack for implementing an animated button without JS (btw, I think that @steve talks about this same hack in one of his TikTok videos).

Still, something y’all might want to look into. Not sure whether Builder is somehow hijacking the label click and forcibly duplicating it when there’s a for attribute, or something like that.

As for why it works in the Visual Editor but not on a real page…no idea! :slight_smile:

Hey Ersin,

Thank you for letting us know about that behavior. Would you mind changing this click action so that it’s using custom Javascript (isMobileMenuOpen = !isMobileMenuOpen)?

I believe that the state toggle UI may be the cause of this issue, but would love for you to check it out and confirm.

Thanks,
~Logan from Builder.io