如何在基于Radix Slot的多态组件中预配置基础framer-motion动画
基于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
相关产品推荐
相关产品推荐

