This component is pretty light-weight. It only contains a button with an SVG and conditionally rendered text. While simple, it still requires a bit of tweaking to make it feel good. I started off by setting up the state and the click handler.
I re-created the iOS theme toggle icon in Figma and then exported it as an SVG. I made sure to structure the SVG into individual paths, so I could animate each path separately.
The original iOS animation is animated in a more complex way. I've tried that as well, but for this interaction I preferred a simpler approach.
The text animation is done using AnimatePresence and animating the y position of each span. I used the popLayout mode to make the exiting element pop out of the page layout, allowing the new element to move into the layout immediately.
The words "Dark" and "Light" aren't the same width. This caused instant changes in the layout. I wanted these changes to be animated. I added the layout prop to the SVG and the "Mode" span.
<motion.span layout>Mode</motion.span>
For the SVG, I created a new motion.div and added the layout prop there, instead of adding the prop directly to the SVG component, as that would interfere with the rotation animation.
Finally, after setting everything up, I added a timeout to the button. Without this, clicking the button quickly can trigger the animation again before it's completed, causing it to start from the wrong direction.