Skip to content

[select] Decouple Select styles from NativeSelect - #49494

Open
dualfroz wants to merge 4 commits into
mui:masterfrom
dualfroz:fix/native-select-icon-open
Open

dualfroz wants to merge 4 commits into
mui:masterfrom
dualfroz:fix/native-select-icon-open

Conversation

@dualfroz

Copy link
Copy Markdown
Contributor

a native select doesnt expose its open state, so NativeSelect can never apply iconOpen. went with michaldudak's suggestion from the issue: the class description now says that, and SelectInput has its own select and icon styles instead of reusing NativeSelectInput's, so the open rotation only lives in Select and MuiNativeSelect theme variants stop styling it

added should not apply MuiNativeSelect theme variants to Select.test.js (fails on master), computed styles of both components in chromium are the same before and after. the copy skips the rules that can only match a native <select>

resolves #35781

SelectInput reused the styled select and icon from NativeSelectInput,
so NativeSelect carried the open icon rotation and the iconOpen class
even though a native select never reports being open, and
MuiNativeSelect theme variants also styled the Select display and icon.

Give SelectInput its own styles, drop the open handling from
NativeSelectInput and document that NativeSelect never applies iconOpen.
@code-infra-dashboard

code-infra-dashboard Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-49494--material-ui.netlify.app/
QR code for https://deploy-preview-49494--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material ▼-41B(-0.01%) ▼-20B(-0.01%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 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

Performance

No regressions in 32 benchmarks · ran 3m 25s · details


Check out the code infra dashboard for more information about this PR.

@ZeeshanTamboli ZeeshanTamboli added the scope: select Changes related to the select. label Oct 26, 2026
Comment thread packages/mui-material/src/NativeSelect/nativeSelectClasses.ts Outdated
@dualfroz

Copy link
Copy Markdown
Contributor Author

michaldudak suggested keeping it with an updated description in the issue, since removing it from NativeSelectClasses would break anyone referencing nativeSelectClasses.iconOpen or overriding it in the theme. happy to remove it if you prefer, or mark it deprecated for removal in v10

@ZeeshanTamboli

Copy link
Copy Markdown
Member

michaldudak suggested keeping it with an updated description in the issue, since removing it from NativeSelectClasses would break anyone referencing nativeSelectClasses.iconOpen or overriding it in the theme. happy to remove it if you prefer, or mark it deprecated for removal in v10

But the class is not applied to native select, right?

@dualfroz

Copy link
Copy Markdown
Contributor Author

right, NativeSelect never has an open state so it's never applied there. removed it

}

const SelectSelect = styled(StyledSelectSelect, {
const SelectSelect = styled('div', {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't need to copy NativeSelect's styles here. The issue only needs the open-icon rotation to live in Select, since Select is the only one with an open state. Can we keep extending StyledSelectSelect like on master (restoring the import and as="div")?

Copying ~110 lines puts the adornment/--_caret logic in two places that have to stay in sync, and it adds +1.41KB parsed to the bundle.

The MuiNativeSelect theme variants leaking into Select is a real bug, but it's separate from #35781. Let's handle it in its own PR if we want to fix it.

Comment on lines +177 to +221
const SelectIcon = styled('svg', {
name: 'MuiSelect',
slot: 'Icon',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.icon, ownerState.open && styles.iconOpen];
},
})({});
})(({ theme }) => ({
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input and to support wrapping tags..
position: 'absolute',
right: 0,
// Center vertically, height is 1em
top: 'calc(50% - .5em)',
// Don't block pointer events on the select under the icon.
pointerEvents: 'none',
color: (theme.vars || theme).palette.action.active,
[`&.${selectClasses.disabled}`]: {
color: (theme.vars || theme).palette.action.disabled,
},
variants: [
{
props: ({ ownerState }) => ownerState.open,
style: {
transform: 'rotate(180deg)',
},
},
{
props: {
variant: 'filled',
},
style: {
right: 7,
},
},
{
props: {
variant: 'outlined',
},
style: {
right: 7,
},
},
],
}));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here: keep extending StyledSelectIcon and only add the rotation you removed from it:

Suggested change
const SelectIcon = styled('svg', {
name: 'MuiSelect',
slot: 'Icon',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.icon, ownerState.open && styles.iconOpen];
},
})({});
})(({ theme }) => ({
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input and to support wrapping tags..
position: 'absolute',
right: 0,
// Center vertically, height is 1em
top: 'calc(50% - .5em)',
// Don't block pointer events on the select under the icon.
pointerEvents: 'none',
color: (theme.vars || theme).palette.action.active,
[`&.${selectClasses.disabled}`]: {
color: (theme.vars || theme).palette.action.disabled,
},
variants: [
{
props: ({ ownerState }) => ownerState.open,
style: {
transform: 'rotate(180deg)',
},
},
{
props: {
variant: 'filled',
},
style: {
right: 7,
},
},
{
props: {
variant: 'outlined',
},
style: {
right: 7,
},
},
],
}));
const SelectIcon = styled(StyledSelectIcon, {
name: 'MuiSelect',
slot: 'Icon',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [styles.icon, ownerState.open && styles.iconOpen];
},
})({
variants: [
{
props: ({ ownerState }) => ownerState.open,
style: {
transform: 'rotate(180deg)',
},
},
],
});

};

export const StyledSelectSelect = styled('select', {
const StyledSelectSelect = styled('select', {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's keep these exported, since SelectInput keeps extending them.

Suggested change
const StyledSelectSelect = styled('select', {
export const StyledSelectSelect = styled('select', {

})({});

export const StyledSelectIcon = styled('svg', {
const StyledSelectIcon = styled('svg', {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here.

Suggested change
const StyledSelectIcon = styled('svg', {
export const StyledSelectIcon = styled('svg', {

Comment on lines +2777 to +2810
it('should not apply MuiNativeSelect theme variants', () => {
const theme = createTheme({
components: {
MuiNativeSelect: {
variants: [
{
props: {
variant: 'outlined',
},
style: {
textTransform: 'uppercase',
},
},
],
},
},
});

const { container } = render(
<ThemeProvider theme={theme}>
<Select variant="outlined" value="first">
<MenuItem value="first" />
<MenuItem value="second" />
</Select>
</ThemeProvider>,
);

expect(container.querySelector(`.${classes.select}`)).not.toHaveComputedStyle({
textTransform: 'uppercase',
});
expect(container.querySelector(`.${classes.icon}`)).not.toHaveComputedStyle({
textTransform: 'uppercase',
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test covers the theme variant leak, not the issue. Since the rotation is the only style that moves, let's test that instead. Select has no test for it today. This passes in both jsdom and Chromium:

Suggested change
it('should not apply MuiNativeSelect theme variants', () => {
const theme = createTheme({
components: {
MuiNativeSelect: {
variants: [
{
props: {
variant: 'outlined',
},
style: {
textTransform: 'uppercase',
},
},
],
},
},
});
const { container } = render(
<ThemeProvider theme={theme}>
<Select variant="outlined" value="first">
<MenuItem value="first" />
<MenuItem value="second" />
</Select>
</ThemeProvider>,
);
expect(container.querySelector(`.${classes.select}`)).not.toHaveComputedStyle({
textTransform: 'uppercase',
});
expect(container.querySelector(`.${classes.icon}`)).not.toHaveComputedStyle({
textTransform: 'uppercase',
});
});
it('should rotate the icon when open', () => {
const { container } = render(<Select open value="" />);
const icon = container.querySelector(`.${classes.icon}`);
expect(icon).to.have.class(classes.iconOpen);
expect(icon).not.toHaveComputedStyle({ transform: 'none' });
});

@ZeeshanTamboli ZeeshanTamboli added the type: bug It doesn't behave as expected. label Oct 26, 2026
@dualfroz

Copy link
Copy Markdown
Contributor Author

done, back to extending StyledSelectSelect/StyledSelectIcon with only the rotation moved to Select, and swapped in your test. ill do the theme variant leak in a separate pr

This branch has not been deployed

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

Labels

scope: select Changes related to the select. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Select] MuiNativeSelect-iconOpen is not applied when control is opened

2 participants