Skip to content

feat: modernize Switch to MD3#4957

Open
adrcotfas wants to merge 2 commits into
mainfrom
@adrcotfas/switch
Open

feat: modernize Switch to MD3#4957
adrcotfas wants to merge 2 commits into
mainfrom
@adrcotfas/switch

Conversation

@adrcotfas
Copy link
Copy Markdown
Collaborator

@adrcotfas adrcotfas commented May 20, 2026

Motivation

Update the Switch to use the latest MD specs, including motion and RTL.

Related issue

Closes #4939
Partially closes #4892

Test plan

  • lint, existing tests and new tests pass
  • manual testing on Android, iOS and web in all states, including hovered and focused

Demo

Screen_recording_20260520_171630.webm
Simulator.Screen.Recording.-.iPhone.16e.-.2026-05-20.at.17.46.36.mov

@github-actions
Copy link
Copy Markdown

The mobile version of example app from this branch is ready! You can see it here.

Comment thread src/components/Switch/Switch.tsx
Comment thread src/components/Switch/Switch.tsx Outdated
Comment thread src/index.tsx Outdated
Comment thread src/components/Switch/Switch.tsx Outdated
Comment thread src/components/Switch/Switch.tsx Outdated
Comment thread src/components/Switch/utils.ts Outdated
Comment thread src/components/__tests__/Switch.test.tsx Outdated
@adrcotfas adrcotfas force-pushed the @adrcotfas/tokens_structure branch from 91d86b4 to be1c417 Compare May 25, 2026 06:21
@adrcotfas adrcotfas added the v6 label May 25, 2026
@adrcotfas adrcotfas force-pushed the @adrcotfas/tokens_structure branch from be1c417 to b41006a Compare May 25, 2026 06:30
@adrcotfas adrcotfas force-pushed the @adrcotfas/switch branch from 562f947 to 7731d0e Compare May 25, 2026 09:11
@adrcotfas adrcotfas requested a review from satya164 May 25, 2026 09:11
@adrcotfas adrcotfas force-pushed the @adrcotfas/tokens_structure branch from b41006a to f675baf Compare May 26, 2026 12:06
Base automatically changed from @adrcotfas/tokens_structure to main May 26, 2026 12:14
@adrcotfas adrcotfas force-pushed the @adrcotfas/switch branch from 7731d0e to 9e41a0b Compare May 26, 2026 13:16
@adrcotfas
Copy link
Copy Markdown
Collaborator Author

@satya164 Findings were fixed. I discovered an issue with the dynamic theme when testing custom switch colors and applied a fix as a separate commit.

Copy link
Copy Markdown
Member

@satya164 satya164 left a comment

Choose a reason for hiding this comment

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

Quickly clicking on the Switch feels bad, probably because the Switch grows and changes position unanimated before the animation is visible

CleanShot.2026-05-26.at.17.00.57-optimised.mp4

RTL seems to be broken on Web

CleanShot.2026-05-26.at.16.39.44-optimised.mp4

I've merged some updates to main which should let you toggle RTL on Web for testing

Comment on lines +164 to +166
const [hovered, setHovered] = React.useState(false);
const [pressed, setPressed] = React.useState(false);
const [focused, setFocused] = React.useState(false);
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.

make these shared values. ideally, we should have event -> animation rather than event -> state update -> effect -> animation. user interactions should be as responsive as possible, even if JS thread is busy.

Comment on lines +402 to +416
<SwitchFocusRing
focused={focused}
style={[
styles.focusRing,
{
borderColor: colors.focusIndicatorColor,
borderWidth: FOCUS_THICKNESS,
top: OVERLAY_TOP + FOCUS_RING_INSET,
left: FOCUS_RING_INSET,
right: FOCUS_RING_INSET,
bottom: OVERLAY_TOP + FOCUS_RING_INSET,
borderRadius: cornerFull,
},
]}
/>
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.

i'd prefer to do this with CSS instead of react state. it'd be a relatively similar amount of code to write these styles inside <style>, and even less code with a reusable utility. is there any benefit to doing this with react state?

we also need to hide the outline from the default styles, as both of them are visible:

Image

Comment on lines +5 to +9
const handleFocus: PressableProps['onFocus'] = (e) => {
if (document.activeElement?.matches(':focus-visible')) {
onFocus?.(e);
}
};
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.

it seems a bit too much abstraction for this small component. the web code could be just a conditional with Platform.OS === 'web' check. in the same component instead of 2 platform specific files, and for such small logic, the whole thing could be inlined and it'd be less code.

@@ -0,0 +1,11 @@
import * as React from 'react';
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.

don't use .web extension. metro supports .native by default. web bundlers don't support .web by default. even though most configs configure it, it also becomes problematic when more tools come into picture such as importing on node (e.g. for SSR).

use .native extension whenever a native/web split is needed.

style: StyleProp<ViewStyle>;
}) {
if (!focused) return null;
return <View pointerEvents="none" style={style} />;
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.

all of the styling logic is in Switch, so a separate component for just a View with pointerEvents="none" seems a bit unnecessary abstraction.

also pointerEvents prop warns on web. check the console warnings. it needs to be astyle.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(switch): align interaction model with MD3 guidelines fix(iOS): Enforce Material Design Consistency

2 participants