Skip to content

M3 styled switch #3797

Closed
Closed

Description

@yuameshi

Is your feature request related to a problem? Please describe.

get a m3 styled switch component

Describe the solution you'd like

just edit stylesheet

Additional context

Official document

Official document

Diff between M2 & M3

Expected style(M3)

Current style(M2)

Activity

  1. changed the title [-]M3 style switch[/-] [+]M3 styled switch[/+] on Apr 2, 2023
  2. yuameshi commented on Apr 4, 2023

    @yuameshi
    Author

    any updates?

  3. lukewalczak commented on Apr 4, 2023

    @lukewalczak
    Member

    Hey @yuameshi, nothing has changed from my last comment related to that topic

  4. crystal7-io commented on Dec 17, 2023

    @crystal7-io

    ----------------------Edit------------------------------------------------------------
    I have commented updated component to match better with Material you in comments bellow this.
    This one had some bugs

    If in case anyone is looking, I have created a "Material You"-like Swtich Component, behaves smoothly, it's touch behaves like Material you Switch. I have followed Material 3 guidelines for making it.
    Here's a video of tapping and draggin actions :

    20231217-160756.mp4

    If anyone likes it, here's the code for component file:

    -------Removing Code as there is updated one at snack here.

    Also install libraries which are imported before using

    npm install react-native-gesture-handler react-native-paper react-native-reanimated
    

    If you don't have reanimated installed, see guide to install reanimated because it needs a couple of extra steps to setup properly.

    It works completely fine with( and only with) React Native paper and changes color and theme too according to paperprovider, tested on Android on Web, though on web, if you are on Desktop, it works fine on tap, but if you try to drag it, it dosent change value. If you wanna run only on mobile, I think this would work great.
    Also there is attribute for icon : <MaterialSwitch icon={"icon_name"}/>, just pass one of MaterialCommunityIcons name as string and it will render a icon when switch is on.

    Hope this heps !

  5. crystal7-io commented on Jan 18, 2024

    @crystal7-io

    @lukewalczak I worked a bit off and created a Material you styled switch with reanimated and gesture handler and it can be used with react native paper,
    here I am using it in my app :

    myapp.mp4

    and here is Files by Google app switch :

    myfiles.mp4

    What do you think, pretty much same ?
    I mean the material you styled switch just hits different, it changes whole look of the ui,
    Should implement this keeping the current switch, users can use anyone which they want.

  6. rollingmoai commented on Jan 19, 2024

    @rollingmoai

    @crystalarcus Can you share the updated component file code? Thanks!

  7. crystal7-io commented on Jan 19, 2024

    @crystal7-io

    @crystalarcus Can you send the updated component file code? Thanks!

    @rollingmoai sure!, actually there are 2 components I have made for personal use : MaterialSwitch and MaterialSwitchList
    as you see in above video, the switch components is many times used with listitem component,
    like you click listitem and it switches the switch. The list item i used above is actualy react-native-paper's List.Item component.
    Sadly it's not quite possible to use List item and gesture handler togther, it causes bugs, so in MaterialSwitchList component, you can't control the switch by holding it and moving your fingers, but even the Switch from Files by Google app, as shown in video above can't do that. You can't control it by holding and moving ur finger to change switch, you just simply need to press it, and whole List.Item is pressed. So it behaves like single component.
    So I had to make 2 separate components to integrate it with List.Item.
    The other component MaterialSwitch, behaves normally like a swtich, you can hold it with fingers, or simply tap it.
    I will share the code for both components in few hours, just need some finishing touch to make it useful for others, cause I just designed it for my personal use.

  8. crystal7-io commented on Jan 19, 2024

    @crystal7-io

    @rollingmoai @lukewalczak So here's it finally completed and read to use, here's a video:

    Android ( Expo Go ) :

    20240119-233030.mp4

    Web (Chrome, Windows , the mouse appears laggy because I was capturing with xbox gamebar) :

    MaterialSwitchReactNativeWeb.mp4

    here's full code :-

    -------Removing Code as there is updated one at snack here.

    Alright, now one thing, I am just a high school student, don't know about all components and attributes, I just used the knowledge I had. Just making this much was a tough job for me, so if you can optimize it, it would be great. Also it works well in my apps that I made for practicing, still if you find any bug, you can try fixing it. I know one bug and its, sometimes rarely if you touch on exact border of switch item, the handle circle will get bigger as if you are holding it and it will only get normal if you tap again on switch. It just happens rarely.
    I gave you a basic foundation that works pretty well, see if you can make it better, if not you can still directly use it !
    Hope it helps someone !

    ( also can someone tell me how to make a snack of a project I currently have in my pc ? I want to put this on snack, would be better to try it yourself before implementing in your app )

  9. crystal7-io commented on Jan 19, 2024

    @crystal7-io

    And here's how to use it :
    Material Switch
    example:

    <MaterialSwitch
                    selected={value}
                    onPress={() => setValue(!value)} 
                    switchOnIcon={'check'}
                    switchOffIcon={'close'}
                    fluid={false}
                    />
    

    Material Switch have 5 attributes, ( you can edit code and add more like colors, etc):

    selected : boolean // value weather switch will be on or off, true : On, false : Off
    onPress : function // function triggered after switch is pressed. ( NOTE: onPress is triggered after the switch changing animation is completed, that is, its passed as a callback to reanimated animation)
    switchOnIcon : string or function which returns a icon component // Icon when switch is on, default is null
    switchOffIcon : string or function which returns a icon component // Icon when switch is off, default is null
    fluid : boolean // fluid like animation when switching, default is false
    

    Material Switch List Item
    example:

      <MaterialSwitchListItem
                  title={"Fluid animation "}
                  listStyle={styles.listItem}
                  fluid={true}
                  selected={value}
                  onPress={() => setValue(!value)} />
    

    This one have 11 props, namely :
    title, description, titleStyle, listStyle, selected, onPress, leftIcon, switchOnIcon, switchOffIcon, rippleColor, fluid
    Out of these, five props : selected, onPress, fluid, switchOnIcon, switchOffIcon, are same from material switch mentioned above.
    the rest six :

    title : "string" for List.Item title (from react-native-paper)
    description : "string" for List.Item description (from react-native-paper)
    listStyle: style for List.Item (from react-native-paper)
    titleStyle: style for Title of List ( you can edit code to change description style, I didnt add it cause it looked fine.)
    leftIcon :  string or function which returns a icon component // Icon to left side of List.Item, default is null
    rippleColor: ripple color of List.Item (from react-native-paper) // default is the one used by List.Item 
    
  10. rollingmoai commented on Jan 20, 2024

    @rollingmoai

    Thanks! Will test this out on iOS when I have time

  11. crystal7-io commented on Jan 20, 2024

    @crystal7-io

    @rollingmoai would like to add one more thing, the state layer on mouse hover:
    unnamed
    I was focused to use it with android, so I didn't gave attention to desktop features. The thing is, the state layer opacity is 0.1 when you click on it, but desktop have mouse, which can be hovered. The opacity on mouse hover is 0.08. Will need to add that for better Desktop experience.

  12. rollingmoai commented on Jan 20, 2024

    @rollingmoai

    It might be good to have a prop to set the duration of the animation as well.

    P.S., Can you also add type annotations for typescript? If not, I can otherwise add them myself. Thanks!

  13. 16 remaining items

  14. crystal7-io commented on Jan 24, 2024

    @crystal7-io

    @rollingmoai I will be quite busy for a few days, I will continue to work on Material switch after that.

  15. micahlt commented on Feb 9, 2024

    @micahlt

    Super glad y'all are working on this!! Thank you so much!

  16. crystal7-io commented on Feb 24, 2024

    @crystal7-io

    @rollingmoai The new Switch Component is ready, changes I did :

    1. MaterialSwitchListItem only shows fluid animation when clicked on ListItem. When clicked on Switch, it shows normal animation.
      ( I did this because that's how Google's Switch work in Google File's App, only shows fluid animation when clicked on ListItem )
    2. Added descriptionStyle to MaterialSwitchListItem.
    3. Optimized fluid animation to be more smoother. ( before, It was smooth on my 8Gb RAM phone, but lagged on old 4GB RAM phone, but now it works smooth on both ).

    Also one more thing, I think we shouldn't add animationTimeScaling prop. It adds complexity to animations due to conditional code. Rather I already changed animation a bit faster so it doesn't feel like waiting for it to complete.
    I think the component is now usable. We can finalize this, what do you think ?
    Here's the snack

  17. crystal7-io commented on Feb 24, 2024

    @crystal7-io

    Here's the snack

    @rollingmoai One more thing about Snack, it's working fine on web, but in expo go app, there's a little problem ONLY when dragging the switch. It cancels touch if you move you finger little out of Switch. But it only happens in that Snack, I am using the Switch component in my app and it's working fine. Maybe it's happening due to ScrollView.
    You can check how its working in my app's settings, here's download link, it's EAS built apk file, you can install and try it in your device. You will find Switch in Theme dialog of settings screen. And a there's group of Switches in Note Type Settings .

  18. crystal7-io commented on Feb 28, 2024

    @crystal7-io

    @yuameshi Hello there, you opened this issue, so I would like you to check this new Material Switch component. You can use it with React Native Paper.

  19. micahlt commented on Feb 28, 2024

    @micahlt

    @lukewalczak any reason this shouldn't be added to React Native Paper officially?

  20. Santhosh-Umapathi commented on Aug 1, 2024

    @Santhosh-Umapathi

    Hi @crystalarcus ,

    Thanks for the amazing work on this.

    Whats the update on this? Was this merged into React Native Paper officially?

  21. crystal7-io commented on Aug 1, 2024

    @crystal7-io

    Whats the update on this? Was this merged into React Native Paper officially?

    @Santhosh-Umapathi It can't be directly merged into React Native Paper but I can make a npm package which u can install and directly use MaterialSwitch component with React Native Paper. Or if you need it urgently you can just copy the Switch Components files from snack

  22. yuameshi commented on Aug 1, 2024

    @yuameshi
    Author

    @yuameshi Hello there, you opened this issue, so I would like you to check this new Material Switch component. You can use it with React Native Paper.

    @crystalarcus sorry for the late response, but i've already use another module called react-native-switch with some cutomization.
    but still thank you for your contributions.

  23. crystal7-io commented on Aug 1, 2024

    @crystal7-io

    @crystalarcus sorry for the late response, but i've already use another module called react-native-switch with some cutomization.

    Ya, initially i considered that option too, but I wanted to add that fluid like animation while switching from Material 3, so I made my won component. It is a good alternative too

  24. Santhosh-Umapathi commented on Aug 1, 2024

    @Santhosh-Umapathi

    Whats the update on this? Was this merged into React Native Paper officially?

    @Santhosh-Umapathi It can't be directly merged into React Native Paper but I can make a npm package which u can install and directly use MaterialSwitch component with React Native Paper. Or if you need it urgently you can just copy the Switch Components files from snack

    Awesome thanks, I will copy the files from the snack. But a npm package could be really helpful for the community as well. If you have free time to work on the package, that would be greatly appreciated :)

  25. sirshad01 commented on Mar 10, 2025

    @sirshad01

    @crystalarcus Have you tried this with expo SDK 52?
    I tried the code from the snack and its working fine on iOS and android but throws a bunch of errors on web.

  26. crystal7-io commented on May 27, 2025

    @crystal7-io

    @crystalarcus Have you tried this with expo SDK 52? I tried the code from the snack and its working fine on iOS and android but throws a bunch of errors on web.

    Haven't used it for long, I will check and see if everything's working fine

  27. lgc2333 commented on Jun 22, 2025

    @lgc2333

    Made a version that switch state strictly follows value prop

    (only tested on Android)

    Details
    import { useCallback, useEffect, useState } from 'react'
    import type { StyleProp, ViewStyle } from 'react-native'
    import { Pressable, StyleSheet, View } from 'react-native'
    import {
      Gesture,
      GestureDetector,
      GestureHandlerRootView,
    } from 'react-native-gesture-handler'
    import { Icon, useTheme } from 'react-native-paper'
    import type { IconSource } from 'react-native-paper/lib/typescript/components/Icon'
    import Animated, {
      Extrapolation,
      interpolate,
      interpolateColor,
      runOnJS,
      useAnimatedStyle,
      useSharedValue,
      withTiming,
    } from 'react-native-reanimated'
    
    export interface Props {
      value: boolean
      onPress?: () => void
      fluid?: boolean
      switchOnIcon?: IconSource // IconSource from 'react-native-paper/lib/typescript/components/Icon'
      switchOffIcon?: IconSource // IconSource from 'react-native-paper/lib/typescript/components/Icon'
      disabled?: boolean
      style?: StyleProp<ViewStyle>
    }
    
    const styles = StyleSheet.create({
      stateOuter: {
        justifyContent: 'center',
        height: 32,
        width: 52,
        alignItems: 'center',
        position: 'absolute',
      },
    })
    
    // https://github.com/callstack/react-native-paper/issues/3797#issuecomment-1859135794
    export default function Switch({
      value,
      onPress,
      switchOnIcon,
      switchOffIcon,
      disabled,
      style,
    }: Props) {
      const theme = useTheme()
      const position = useSharedValue(value ? 10 : -10)
      const handleHeight = useSharedValue(value ? 24 : 16)
      const handleWidth = useSharedValue(value ? 24 : 16)
      const [isPressed, setIsPressed] = useState(false)
    
      const syncButtonState = useCallback(() => {
        position.value = withTiming(value ? 10 : -10, { duration: 250 })
        handleHeight.value = withTiming(value ? 24 : 16, { duration: 100 })
        handleWidth.value = withTiming(value ? 24 : 16, { duration: 100 })
      }, [value])
    
      const callbackFunction = () => {
        onPress?.()
        syncButtonState()
      }
    
      const pan = Gesture.Pan()
        .activateAfterLongPress(100)
        .enabled(!disabled)
        .onTouchesUp(() => setIsPressed(false))
        .runOnJS(true)
        .hitSlop(disabled ? -30 : 0)
        .onStart(() => {
          setIsPressed(true)
          handleHeight.value = withTiming(28, { duration: 160 })
          handleWidth.value = withTiming(28, { duration: 160 })
        })
        .onChange((event) => {
          if (position.value + event.translationX / 10 < -10) {
            position.value = -10
            return
          }
          if (position.value + event.translationX / 10 > 10) {
            position.value = 10
            return
          }
          position.value += event.translationX / 10
        })
        .onEnd(() => {
          setIsPressed(false)
          if (position.value > 0) {
            position.value = withTiming(10)
            handleHeight.value = withTiming(24, { duration: 160 })
            handleWidth.value = withTiming(24, { duration: 160 })
            if (!value) runOnJS(callbackFunction)()
          } else if (position.value < 0) {
            position.value = withTiming(-10)
            handleHeight.value = withTiming(16, { duration: 160 })
            handleWidth.value = withTiming(16, { duration: 160 })
            if (value) runOnJS(callbackFunction)()
          }
        })
      // #endregion
    
      const handleStyle = useAnimatedStyle(() =>
        disabled
          ? {
              transform: [{ translateX: value ? 10 : -10 }],
              height: value ? 24 : 16,
              width: value ? 24 : 16,
              marginVertical: 'auto',
              minHeight: switchOffIcon ? 24 : 16,
              minWidth: switchOffIcon ? 24 : 16,
              opacity: value ? 1 : 0.36,
              backgroundColor: value ? theme.colors.surface : theme.colors.onSurface,
              borderRadius: 20,
              justifyContent: 'center',
              alignItems: 'center',
            }
          : {
              transform: [{ translateX: position.value }],
              opacity: 1,
              height: handleHeight.value,
              width: handleWidth.value,
              marginVertical: 'auto',
              minHeight: switchOffIcon ? 24 : 16,
              minWidth: switchOffIcon ? 24 : 16,
              backgroundColor: interpolateColor(
                position.value,
                [-10, 10],
                [theme.colors.outline, theme.colors.onPrimary],
              ),
              borderRadius: 20,
              justifyContent: 'center',
              alignItems: 'center',
            },
      )
      const trackStyle = useAnimatedStyle(() =>
        disabled
          ? {
              borderWidth: 2,
              borderRadius: 16,
              justifyContent: 'center',
              height: 32,
              width: 52,
              opacity: 0.12,
              backgroundColor: value ? theme.colors.onSurface : theme.colors.surfaceVariant,
              borderColor: theme.colors.onSurface,
            }
          : {
              alignItems: 'center',
              opacity: 1,
              backgroundColor: interpolateColor(
                position.value,
                [-10, 10],
                [theme.colors.surfaceVariant, theme.colors.primary],
              ),
              borderColor: interpolateColor(
                position.value,
                [-10, 10],
                [theme.colors.outline, theme.colors.primary],
              ),
              borderWidth: 2,
              borderRadius: 16,
              justifyContent: 'center',
              height: 32,
              width: 52,
            },
      )
      const handleOutlineStyle = useAnimatedStyle(() => ({
        height: 40,
        width: 40,
        borderRadius: 20,
        position: 'absolute',
        transform: [{ translateX: position.value }],
        backgroundColor: !isPressed
          ? 'transparent'
          : interpolateColor(
              position.value,
              [-10, 10],
              [theme.colors.onSurface, theme.colors.primary],
            ),
        alignItems: 'center',
        opacity: 0.18,
        justifyContent: 'center',
      }))
      const iconOnStyle = useAnimatedStyle(() => ({
        opacity: disabled
          ? 0.38
          : interpolate(position.value, [0, 10], [0, 1], Extrapolation.CLAMP),
        overflow: 'hidden',
        transform: [
          {
            scale: interpolate(position.value, [0, 10], [0, 1], Extrapolation.CLAMP),
          },
          {
            rotate: `${interpolate(
              position.value,
              [0, 10],
              [-90, 0],
              Extrapolation.CLAMP,
            )}deg`,
          },
        ],
      }))
      const iconOffStyle = useAnimatedStyle(() => ({
        position: 'absolute',
        opacity: disabled
          ? 0.38
          : interpolate(position.value, [-10, 0], [1, 0], Extrapolation.CLAMP),
        overflow: 'hidden',
        transform: [
          {
            scale: interpolate(position.value, [-10, 0], [1, 0], Extrapolation.CLAMP),
          },
          {
            rotate: `${interpolate(
              position.value,
              [-10, 0],
              [-90, 0],
              Extrapolation.CLAMP,
            )}deg`,
          },
        ],
        // width: interpolate(position.value, [5, 10], [0, 16]),
      }))
    
      useEffect(() => {
        syncButtonState()
      }, [value])
    
      return (
        <View style={[{ borderRadius: 20, backgroundColor: theme.colors.surface }, style]}>
          <View pointerEvents="none" style={styles.stateOuter}>
            <Animated.View style={handleOutlineStyle} key={3}></Animated.View>
          </View>
          <Animated.View style={trackStyle} key={1}>
            <GestureHandlerRootView>
              <GestureDetector gesture={pan}>
                <Pressable
                  disabled={disabled}
                  style={{
                    justifyContent: 'center',
                    height: 32,
                    width: 52,
                    alignItems: 'center',
                  }}
                  onPress={() => {
                    setIsPressed(true)
                    try {
                      callbackFunction()
                    } finally {
                      setIsPressed(false)
                    }
                  }}
                ></Pressable>
              </GestureDetector>
            </GestureHandlerRootView>
          </Animated.View>
          <View pointerEvents="none" style={styles.stateOuter}>
            <Animated.View style={handleStyle} key={2}>
              {switchOnIcon ? (
                <Animated.View key={10} style={iconOnStyle}>
                  <Icon
                    source={switchOnIcon}
                    size={16}
                    color={
                      disabled ? theme.colors.onSurface : theme.colors.onPrimaryContainer
                    }
                  />
                </Animated.View>
              ) : null}
              {switchOffIcon ? (
                <Animated.View key={9} style={iconOffStyle}>
                  <Icon source={switchOffIcon} size={16} color={theme.colors.surface} />
                </Animated.View>
              ) : null}
            </Animated.View>
          </View>
        </View>
      )
    }
  28. rollingmoai commented on May 29, 2026

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions