You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Framer Motion自定义Switch组件的动画闪烁与字体缩放问题

自定义Switch组件问题修复:动画闪烁+字体自适应失效

一、Framer Motion动画闪烁/元素乱跑的问题

问题原因

你给两个子元素都加了layout="position",同时父元素通过flex-row/flex-row-reverse反转布局,这会让Framer Motion的布局动画计算冲突,导致元素位置跳变、闪烁。flex方向的突然切换会打乱motion的布局追踪逻辑。

修复方案

放弃通过flex方向反转实现切换,改用控制滑块的x偏移来实现滑动效果,同时移除子元素的layout="position",给父元素固定flex方向,让布局逻辑更稳定:

  1. 父元素固定使用flex-row,不再切换方向
  2. 滑块通过animate.x属性控制位置,根据checked状态设置偏移值
  3. 标签文本直接切换内容,避免多余的布局动画触发
  4. 添加明确的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 13:30:39