Skip to content

[DropdownMenu] Native form submission via form attribute doesn't work with asChild unless child has explicit onClick handler #3789

@elliotbonneville

Description

@elliotbonneville

Bug report

Current Behavior

When using a <button type="submit" form="form-id"> inside a Radix component with asChild, native form submission via the HTML form attribute doesn't work. The dropdown closes but the form is not submitted.

Adding an explicit onClick handler (even a no-op onClick={() => {}}) to the button makes native form submission work correctly.

Expected behavior

A submit button with a form attribute should trigger native form submission when clicked, regardless of whether an explicit onClick handler is provided.

Reproducible example

import { DropdownMenu } from '@radix-ui/react-dropdown-menu';
 function Example() {
  return (
    <>
      <DropdownMenu.Root>
        <DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
        <DropdownMenu.Content>
          <DropdownMenu.Item asChild>
            {/* Does NOT submit the form */}
            <button type="submit" form="my-form">Submit</button>
             {/* Adding onClick={() => {}} DOES submit the form */}
            {/* <button type="submit" form="my-form" onClick={() => {}}>Submit</button> */}
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Root>
       <form id="my-form" action="/submit" method="post" />
    </>
  );
}

Suggested solution

The issue appears to be in packages/react/slot/src/slot.tsx in the mergeProps function. Event handlers are only composed when both parent and child have them:

if (isHandler) {
  if (slotPropValue && childPropValue) {
    // Both exist: composed - native form submission WORKS
    overrideProps[propName] = (...args) => {
      childPropValue(...args);
      slotPropValue(...args);
    };
  } else if (slotPropValue) {
    // Only slot has handler - native form submission BROKEN
    overrideProps[propName] = slotPropValue;
  }
}

When only the slot's handler exists, native browser behavior is somehow prevented. When handlers are composed, native behavior is preserved.

Note: A wrapper component workaround does not work:

  const FormButton = forwardRef(({ onClick, ...props }, ref) => (
    <button ref={ref} onClick={onClick ?? (() => {})} {...props} />
  ));

  // This does NOT work - Slot merges with FormButton's props, not the rendered button's props
  <DropdownMenu.Item asChild>
    <FormButton type="submit" form="my-form">Submit</FormButton>
  </DropdownMenu.Item>

Because Slot uses children.props (the immediate child element's props), not the rendered output's props, the internal onClick never participates in the merge.

The fix may involve ensuring the slot-only handler path doesn't interfere with native form submission, or always wrapping handlers in a way that preserves native behavior.

Additional context

This affects any use case where a native HTML form button is used inside a Radix component with asChild, particularly when the form element is outside the dropdown (to avoid DOM disconnection when the dropdown closes).

Your environment

Software Name(s) Version
Radix Package(s) @radix-ui/react-slot, @radix-ui/react-dropdown-menu
React n/a 19
Browser Chrome
Assistive tech
Node n/a 22
npm/yarn/pnpm npm
Operating System macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions