diff --git a/change/@fluentui-react-button-01f35392-a207-47c2-9914-bcd12d1e85ce.json b/change/@fluentui-react-button-01f35392-a207-47c2-9914-bcd12d1e85ce.json new file mode 100644 index 00000000000000..9b56d5c1fd5544 --- /dev/null +++ b/change/@fluentui-react-button-01f35392-a207-47c2-9914-bcd12d1e85ce.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(toggle-button): prevent outline toggle from increasing button size", + "packageName": "@fluentui/react-button", + "email": "robertpenner@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.ts b/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.ts index 631abe51d33911..6df2f10cc65b09 100644 --- a/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.ts +++ b/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.ts @@ -14,6 +14,14 @@ export const toggleButtonClassNames: SlotClassNames = { icon: 'fui-ToggleButton__icon', }; +const useRootAppearanceStyles = makeStyles({ + // Appearance variations that apply regardless of checked state + outline: { + // Ensure box-shadow transitions in both directions (checked <-> unchecked) + transitionProperty: 'background, border, color, box-shadow', + }, +}); + const useRootCheckedStyles = makeStyles({ // Base styles base: { @@ -74,18 +82,23 @@ const useRootCheckedStyles = makeStyles({ outline: { backgroundColor: tokens.colorTransparentBackgroundSelected, ...shorthands.borderColor(tokens.colorNeutralStroke1), - ...shorthands.borderWidth(tokens.strokeWidthThicker), + // Use inset box-shadow to create thicker border visual without affecting layout + boxShadow: `0 0 0 ${tokens.strokeWidthThick} ${tokens.colorNeutralStroke1} inset`, ':hover': { backgroundColor: tokens.colorTransparentBackgroundHover, + boxShadow: `0 0 0 ${tokens.strokeWidthThick} ${tokens.colorNeutralStroke1Hover} inset`, }, ':hover:active,:active:focus-visible': { backgroundColor: tokens.colorTransparentBackgroundPressed, + boxShadow: `0 0 0 ${tokens.strokeWidthThick} ${tokens.colorNeutralStroke1Pressed} inset`, }, ...createCustomFocusIndicatorStyle({ - ...shorthands.borderColor(tokens.colorNeutralStroke1), + ...shorthands.borderColor(tokens.colorStrokeFocus2), + // Stack focus indicator (strokeWidthThin) on top of the grey border (strokeWidthThick) + boxShadow: `0 0 0 ${tokens.strokeWidthThin} ${tokens.colorStrokeFocus2} inset, 0 0 0 ${tokens.strokeWidthThick} ${tokens.colorNeutralStroke1} inset`, }), }, primary: { @@ -252,6 +265,7 @@ const usePrimaryHighContrastStyles = makeStyles({ export const useToggleButtonStyles_unstable = (state: ToggleButtonState): ToggleButtonState => { 'use no memo'; + const rootAppearanceStyles = useRootAppearanceStyles(); const rootCheckedStyles = useRootCheckedStyles(); const rootDisabledStyles = useRootDisabledStyles(); const iconCheckedStyles = useIconCheckedStyles(); @@ -262,6 +276,9 @@ export const useToggleButtonStyles_unstable = (state: ToggleButtonState): Toggle state.root.className = mergeClasses( toggleButtonClassNames.root, + // Appearance base styles (applies to both checked and unchecked) + appearance === 'outline' && rootAppearanceStyles.outline, + // Primary high contrast styles appearance === 'primary' && primaryHighContrastStyles.base, appearance === 'primary' && (disabled || disabledFocusable) && primaryHighContrastStyles.disabled,