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

如何在基于Radix Slot的多态组件中预配置基础framer-motion动画

解决Radix Slot + Framer Motion多态Button的TypeScript类型冲突问题

基于Radix UI的Slot组件可实现支持单元素的多态Button,现在需要给该组件添加Framer Motion的点击缩放(缩至0.9)动画,但用motion包裹Slot时出现TypeScript类型冲突,相关代码及错误如下:

基础多态Button组件

// your-button.jsx
import React from 'react';
import { Slot } from '@radix-ui/react-slot';

function Button({ asChild, ...props }) {
  const Comp = asChild ? Slot : 'button';
  return <Comp {...props} />;
}

需求

让多态Button组件自带点击时缩放至0.9的Framer Motion动画,纯Motion实现示例如下:

function Button({ ...props }) {
  return <motion.button whileTap={{ scale: 0.9 }} {...props} />;
}

尝试方案及错误

尝试用motion高阶组件包裹Slot,代码如下:

export interface ButtonProps extends HTMLMotionProps<'div'> {
  asChild?: boolean;
}

const MotionSlot = motion(Slot);

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ asChild = false, ...props }, ref) => {
    const Comp = asChild ? MotionSlot : motion.button;

    return <Comp whileTap={{ scale: 0.9 }} ref={ref} {...props} />;
  }
);

Button.displayName = 'Button';

触发的TypeScript错误:

Type '{ className?: string | undefined; title?: string | undefined; defaultChecked?: boolean | undefined; defaultValue?: string | number | readonly string[] | undefined; suppressContentEditableWarning?: boolean | undefined; ... 314 more ...; ref: ForwardedRef<...>; }' is not assignable to type 'Omit<SlotProps & RefAttributes<HTMLElement> & MotionProps, "ref">'.
  Types of property 'style' are incompatible.
    Type 'MotionStyle | undefined' is not assignable to type '(CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>) | ...'.
      Type 'MotionStyle' is not assignable to type '(CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>) | ...'.
        Type 'MotionStyle' is not assignable to type 'CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>'.
          Type 'MotionStyle' is not assignable to type 'CSSProperties'.
            Types of property 'accentColor' are incompatible.
              Type 'MotionValue<number> | MotionValue<string> | CustomValueType | MotionValue<any> | AccentColor | undefined' is not assignable to type 'AccentColor | undefined'.
                Type 'MotionValue<number>' is not assignable to type 'AccentColor | undefined'.ts(2322)

解决方案

问题根源在于类型定义不匹配:HTMLMotionProps<'div'>无法同时兼容Slot组件和motion.button的属性,且ref类型未根据渲染目标动态调整。

1. 正确定义组件Props

使用联合类型区分asChild不同状态下的属性,确保类型精准匹配:

import React, { forwardRef, ForwardedRef } from 'react';
import { Slot, SlotProps } from '@radix-ui/react-slot';
import { motion, HTMLMotionProps, MotionProps } from 'framer-motion';

// 区分asChild为true/false时的属性集合
type ButtonProps = (
  | ({ asChild?: false } & HTMLMotionProps<'button'>)
  | ({ asChild: true } & SlotProps & MotionProps)
);

2. 动态适配Ref类型

通过泛型让ref类型根据实际渲染的元素自动适配:

const Button = forwardRef(function Button<T extends HTMLElement = HTMLButtonElement>(
  { asChild = false, ...props }: ButtonProps,
  ref: ForwardedRef<T>
) {
  const MotionComponent = asChild ? motion(Slot) : motion.button;
  return <MotionComponent whileTap={{ scale: 0.9 }} ref={ref} {...props} />;
});

Button.displayName = 'Button';

完整可运行代码

import React, { forwardRef, ForwardedRef } from 'react';
import { Slot, SlotProps } from '@radix-ui/react-slot';
import { motion, HTMLMotionProps, MotionProps } from 'framer-motion';

type ButtonProps = (
  | ({ asChild?: false } & HTMLMotionProps<'button'>)
  | ({ asChild: true } & SlotProps & MotionProps)
);

const Button = forwardRef(function Button<T extends HTMLElement = HTMLButtonElement>(
  { asChild = false, ...props }: ButtonProps,
  ref: ForwardedRef<T>
) {
  const MotionComponent = asChild ? motion(Slot) : motion.button;
  return <MotionComponent whileTap={{ scale: 0.9 }} ref={ref} {...props} />;
});

Button.displayName = 'Button';

export default Button;

关键说明

  • 联合类型拆分asChild不同状态的Props,避免属性类型交叉冲突;
  • 泛型ref支持动态适配按钮元素或Slot包裹的子元素类型;
  • 直接在组件内创建motion(Slot),无需单独定义变量,简化代码结构。

内容的提问来源于stack exchange,提问作者Bruno Kawka

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 22:35:55