Skip to content

[ButtonBase] Add nativeButton prop#47989

Open
mj12albert wants to merge 11 commits intomui:masterfrom
mj12albert:refactor/button-base-hook
Open

[ButtonBase] Add nativeButton prop#47989
mj12albert wants to merge 11 commits intomui:masterfrom
mj12albert:refactor/button-base-hook

Conversation

@mj12albert
Copy link
Member

@mj12albert mj12albert commented Mar 16, 2026

Currently this JSX

const CustomButton = React.forwardRef<HTMLButtonElement>(function CustomButton(
  props,
  ref
) {
  return <button ref={ref} {...props} />;
});

<Button component={CustomButton} disabled>
  Custom
</Button>

Renders this incorrect HTML: https://stackblitz.com/edit/7wy4fcg4?file=src%2FDemo.tsx

<button tabindex="-1" role="button" aria-disabled="true">Custom</button>

Continuing from #47985, the component === 'button' heuristic doesn't work for when component is a React component; an explicit nativeButton: boolean prop is required upfront to render the correct HTML, with the constraint that the attributes have to be resolved before mount (so there is no DOM to check)

Who is affected and how:

A dev warning will be shown to users who are either:

  • replacing a native button with a component that resolves to a non-<button> element (e.g. component={MySpan}), or
  • replacing a non-native button (e.g. a div) with a component that resolves to a <button> element

e.g.

  • Button component={CustomButton} -> no warning if it resolves to <button>
  • ⚠️ Button component={StyledSpan} -> warn unless nativeButton={false} is specified
  • MenuItem component={CustomLi} -> no warning if it doesn't resolve to <button>
  • ⚠️ MenuItem component={CustomButton} -> warn unless nativeButton={true}

Affected components:

  • Button, Fab, IconButton, ListItemButton, MenuItem, StepButton, Tab, ToggleButton, AccordionSummary, BottomNavigationAction, CardActionArea, TableSortLabel inherit the nativeButton prop via ButtonBase
  • PaginationItem doesn't use ExtendButtonBaseTypeMap for some reason so the type/proptype needed to be manually added
  • Chip conditionally forwards nativeButton depending on whether it's a link (backward compatibility with "legacy" mode)

Preview

Added a docs section in the Button page: https://deploy-preview-47989--material-ui.netlify.app/material-ui/react-button/#rendering-non-native-buttons

@mj12albert mj12albert added the component: ButtonBase The React component. label Mar 16, 2026
@mui-bot
Copy link

mui-bot commented Mar 16, 2026

Netlify deploy preview

Bundle size report

Bundle Parsed size Gzip size
@mui/material 🔺+2.1KB(+0.41%) 🔺+711B(+0.49%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against de25a79

@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 53c397d to 0396ac9 Compare March 16, 2026 09:02
@siriwatknp
Copy link
Member

I am good with the new prop but we should have a clear example (ideally real scenario) to update the docs too.

@zannager zannager added the scope: button Changes related to the button. label Mar 16, 2026
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Mar 16, 2026
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 51fc75b to b0f14e2 Compare March 16, 2026 14:47
@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Mar 16, 2026
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from b0f14e2 to 2e14d7f Compare March 17, 2026 14:46
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Mar 17, 2026
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch 4 times, most recently from f84fa2b to 6b83890 Compare March 17, 2026 16:27
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 6b83890 to ae47aa4 Compare March 17, 2026 16:56
Comment on lines +259 to +262
if (disabled) {
event.preventDefault();
return;
}
Copy link
Member Author

@mj12albert mj12albert Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@siriwatknp Following up on the comment in #47985 (comment)

The additional click guard + return here would only affect anybody counting on programmatically focusing and clicking a disabled non-native ButtonBase-based component then also expecting the click handler to run, similar to #48003

@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 9dc6ca8 to 0562586 Compare March 17, 2026 21:24
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 0562586 to f4b40dc Compare March 17, 2026 21:49
@mj12albert
Copy link
Member Author

I am good with the new prop but we should have a clear example (ideally real scenario) to update the docs too.

Added a brief section here ~

@mj12albert mj12albert added accessibility a11y type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. and removed accessibility a11y labels Mar 18, 2026
Comment on lines +57 to +66
/**
* An additional function that will run before the user's `onKeyDown`, e.g.
* to trigger the ripple effect in `<ButtonBase>`.
*/
onBeforeKeyDown?: React.KeyboardEventHandler<any> | undefined;
/**
* An additional function that will run before the user's `onKeyDown`, e.g.
* to control the ripple effect in `<ButtonBase>`.
*/
onBeforeKeyUp?: React.KeyboardEventHandler<any> | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These custom onBeforeKeyX handlers are a bit odd, but it's solely for controlling the ripple

tabIndex={disabled ? -1 : tabIndex}
type={type}
{...buttonProps}
{...(isLink ? linkProps : buttonProps)}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<ButtonBase> needs to allow links for compatibility reasons, but the useButtonBase hook does not and only covers button/button-likes

@mj12albert mj12albert marked this pull request as ready for review March 18, 2026 05:08
@mj12albert
Copy link
Member Author

mj12albert commented Mar 18, 2026

For the upgrade guide, it would be tidier and less noisy to document all of this under ### ButtonBase instead of adding a h3 for every single affected component – preview link

What do you think? @siriwatknp @silviuaavram

@mj12albert mj12albert changed the title [ButtonBase] Extract a hook and add nativeButton prop [ButtonBase] Add nativeButton prop Mar 18, 2026
@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from 5bb437e to 9322548 Compare March 18, 2026 05:17
// replaces internal handling in Chip, other components can opt-in individually to use this in the future
focusableWhenDisabled,
// private prop to allow native vs non-native button props to be resolved before mount
defaultNativeButton: defaultNativeButtonProp,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to name it as internalNativeButton. I use "internal" in MUI X too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 04ddead

@mj12albert mj12albert force-pushed the refactor/button-base-hook branch from be48251 to 0e3a9a6 Compare March 18, 2026 11:04

#### Replacing native button elements with non-interactive elements

The `nativeButton` prop is available on `<ButtonBase>` and all button-like components to ensure that they are rendered with the correct HTML attributes before hydration, for example during server-side rendering. This should be specified when when passing a React component to the `component` prop of a button-like component. This should be specified if the custom component either replaces the default rendered element:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can improve this phrasing, there's a double "when" and last 2 sentences start in the same way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in de25a79


## Rendering non-native buttons

The `nativeButton` prop can be used to allow buttons to remain keyboard accessible when passing a React component to the [`component`](/material-ui/guides/composition/#passing-other-react-components) prop that renders a non-interactive element like a `<div>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, this information was not enough to understand fully what's going on. Maybe we need to have another example with the other case, when nativeButton is true and custom component is actually a button. Also, a an example when the prop is true and custom component returns not a button, we mention the warning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to have another example with the other case, when nativeButton is true and custom component is actually a button.

I think this is pretty rare in practice (e.g. making a MenuItem render a <button> elem) and not worth documenting, the dev warning should be self explanatory enough ~

an example when the prop is true and custom component returns not a button

Same here, the dev warnings should be enough for the user to correct the issue without having to go to the docs; only button -> span/div is worth documenting here as it's the more common use-case among the various dev warning combinations

},
}),
[],
[buttonRef],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should refs be included in deps array?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just to satisfy eslint and has no runtime use, because the buttonRef is returned by the hook and isn't in the local scope anymore

*/
LinkComponent: PropTypes.elementType,
/**
* Whether the custom component should render a native `<button>` element when
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom component already renders whatever the user tells it to render. The nativeButton only allows non-buttons to retain button a11y. Or at least that's what I understood.

Copy link
Member Author

@mj12albert mj12albert Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of should, how about Whether the custom component is expected to render…?

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

Labels

component: ButtonBase The React component. scope: button Changes related to the button. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants