Framer Motion自定义Switch组件的动画闪烁与字体缩放问题
自定义Switch组件问题修复:动画闪烁+字体自适应失效
一、Framer Motion动画闪烁/元素乱跑的问题
问题原因
你给两个子元素都加了layout="position",同时父元素通过flex-row/flex-row-reverse反转布局,这会让Framer Motion的布局动画计算冲突,导致元素位置跳变、闪烁。flex方向的突然切换会打乱motion的布局追踪逻辑。
修复方案
放弃通过flex方向反转实现切换,改用控制滑块的x偏移来实现滑动效果,同时移除子元素的layout="position",给父元素固定flex方向,让布局逻辑更稳定:
- 父元素固定使用
flex-row,不再切换方向 - 滑块通过
animate.x属性控制位置,根据checked状态设置偏移值 - 标签文本直接切换内容,避免多余的布局动画触发
- 添加明确的
transition参数,让滑动动画更平滑自然
二、clamp()字体自适应无效的问题
问题原因
你写的clamp(10px, 12px, 14px)逻辑错误:clamp的第二个参数是首选值,如果这个值是固定像素,clamp会直接取这个值,不会实现自适应缩放。必须用相对单位(比如vw、基于容器宽度的百分比)作为中间值,才能让字体随空间变化调整大小。
修复方案
把中间值改成相对单位,比如基于视窗宽度的vw,或者结合容器宽度的动态计算值。如果要让字体精准适配Switch组件的空间,可以给父元素设置最小宽度,再用clamp(10px, 1.5vw, 14px)作为字体大小;或者通过React的useRef+useEffect动态计算容器宽度,再调整字体大小。
修复后的完整代码
import React from "react"; import { clsx } from "@mantine/core"; import { motion } from "framer-motion"; type Props = { checked: boolean; onChange: () => void; labelChecked: string; labelUnchecked: string; }; export const AppSwitch: React.FC<Props> = ({ checked, onChange, labelChecked, labelUnchecked }) => { // 滑块偏移值:根据标签实际宽度调整,示例为24px const sliderOffset = checked ? 24 : 0; return ( <motion.div onClick={onChange} animate={{ backgroundColor: checked ? "#68A4F5" : "#70707015" }} className="cursor-pointer select-none shadow-md inline-flex items-center rounded-xl p-0.5 flex-row" style={{ minWidth: "80px" }} // 设置最小宽度避免布局跳动 > {/* 标签:固定位置,直接切换内容 */} <div className="pl-3 pr-2 uppercase font-semibold text-black" style={{ fontSize: "clamp(10px, 1.5vw, 14px)" }} // 修正后的自适应字体 > {checked ? labelChecked : labelUnchecked} </div> {/* 滑块:通过x偏移实现平滑滑动 */} <motion.div animate={{ x: sliderOffset }} transition={{ type: "spring", stiffness: 400, damping: 30 }} className="w-6 h-6 bg-white rounded-full ml-auto" /> </motion.div> ); };
如果需要更精准的字体自适应(比如完全适配Switch容器宽度),可以用动态计算方式:
import React, { useRef, useEffect, useState } from "react"; import { clsx } from "@mantine/core"; import { motion } from "framer-motion"; type Props = { checked: boolean; onChange: () => void; labelChecked: string; labelUnchecked: string; }; export const AppSwitch: React.FC<Props> = ({ checked, onChange, labelChecked, labelUnchecked }) => { const [fontSize, setFontSize] = useState("12px"); const switchRef = useRef<HTMLDivElement>(null); const sliderOffset = checked ? 24 : 0; // 动态计算字体大小 useEffect(() => { if (switchRef.current) { const containerWidth = switchRef.current.offsetWidth; // 取容器宽度的15%,同时限制在10px-14px之间 const calculatedSize = Math.max(10, Math.min(14, containerWidth * 0.15)); setFontSize(`${calculatedSize}px`); } }, [checked, labelChecked, labelUnchecked]); return ( <motion.div ref={switchRef} onClick={onChange} animate={{ backgroundColor: checked ? "#68A4F5" : "#70707015" }} className="cursor-pointer select-none shadow-md inline-flex items-center rounded-xl p-0.5 flex-row" style={{ minWidth: "80px" }} > <div className="pl-3 pr-2 uppercase font-semibold text-black" style={{ fontSize }} > {checked ? labelChecked : labelUnchecked} </div> <motion.div animate={{ x: sliderOffset }} transition={{ type: "spring", stiffness: 400, damping: 30 }} className="w-6 h-6 bg-white rounded-full ml-auto" /> </motion.div> ); };
内容的提问来源于stack exchange,提问作者ScreamZ
相关产品推荐
相关产品推荐

