Back to TIL
March 2026
posted on 03.29.2026

HTML Button Nesting Causes Hydration Errors

In HTML, <button> cannot be a descendant of another <button>. Browsers silently restructure the DOM when they encounter this, but React's server-rendered HTML does not match the browser's restructured DOM, causing a hydration mismatch.

// BAD: nested button causes hydration error
<button onClick={openMenu}>
  <Icon />
  {isOpen && (
    <div className="submenu">
      <button onClick={selectOption}>Option A</button>  {/* nested! */}
    </div>
  )}
</button>
// GOOD: move submenu outside the button, wrap both in a div
<div onMouseEnter={showMenu} onMouseLeave={hideMenu}>
  <button onClick={openMenu}>
    <Icon />
  </button>
  {isOpen && (
    <div className="submenu">
      <button onClick={selectOption}>Option A</button>
    </div>
  )}
</div>

The error message in Next.js dev mode is: In HTML, <button> cannot be a descendant of <button>. This will cause a hydration error.

This also applies to <a> inside <a>, <form> inside <form>, and other elements with restrictions defined in the HTML spec's content model.

No reactions yet

in Naperville, IL
Last visitor from Mitaka, Japan
⌘K