sixfwa / shiny-button

25 stars 10 forks source link

Fixing Framer Motion TypeScript Definitions To Allow Custom CSS Variables in `style` Property #1

Open mazwrld opened 2 months ago

mazwrld commented 2 months ago

Issue: Framer Motion TypeScript Definitions Should Allow Custom CSS Variables in style Property

Description

The current TypeScript definitions for Framer Motion do not allow for the use of custom CSS variables in the style property of initial and animate objects. This limitation causes TypeScript errors when using custom CSS variables in these properties. The error message received is: "Object literal may only specify known properties, and ''--x'' does not exist in type 'string[] | AnimationControls | TargetAndTransition'."

Proposed Fix

Modify the TypeScript definitions to allow for custom CSS variables in the style property by using a custom type definition for the style property.

Steps to Reproduce

  1. Use custom CSS variables in the style property of initial or animate objects in a Framer Motion component.
  2. Run TypeScript checks or linting with ESLint.
  3. Observe TypeScript errors related to the use of custom CSS variables.

Example


import { motion, MotionProps } from 'framer-motion'

type CustomStyle = {
  '--x': string;
};

type CustomMotionProps = MotionProps & {
  initial: {
    style: CustomStyle;
    scale: number;
  };
  animate: {
    style: CustomStyle;
  };
};

const initialProps: CustomMotionProps = {
  initial: {
    style: {
      '--x': '100%',
    },
    scale: 1,
  },
  animate: {
    style: {
      '--x': '-100%',
    },
  },
};

// Use `initialProps` in a Framer Motion component
estebanarriaga commented 2 months ago

Seems like a good solution, but it didn't work for me.

I avoided the typescript error just by adding a @ts-ignore flag on top of both errors. Like so:

const ShinyButton = () => {
  return (
    <motion.button
      // @ts-ignore
      initial={{ "--x": "100%", scale: 1 }}
      // @ts-ignore
      animate={{ "--x": "-100%" }}
      whileTap={{ scale: 0.97 }}
      transition={{
        repeat: Infinity,
        repeatType: "loop",
        repeatDelay: 1,
        type: "spring",
        stiffness: 20,
        damping: 15,
        mass: 2,
        scale: {
          type: "spring",
          stiffness: 10,
          damping: 5,
          mass: 0.1,
        },
      }}
      className="px-6 py-2 rounded-md relative radial-gradient"
    >
      <span className="text-neutral-100 tracking-wide font-light h-full w-full block relative linear-mask">
        Start now
      </span>
      <span className="block absolute inset-0 rounded-md p-px linear-overlay" />
    </motion.button>
  );
};

It will build with no errors. Hope it works!

mazwrld commented 2 months ago

Oh, I totally missed a bug because I didn't test the code in my repo. My bad!

I've fixed it now. In my case, using @ts-ignore would have worked, but my eslint rules are pretty strict, so I had to come up with a different solution.


type ExtendedMotionProps = HTMLMotionProps<'button'> & {
  initial?: Record<string, unknown>
  animate?: Record<string, unknown>
}

type ShinyButtonProps = ExtendedMotionProps & {
  children: React.ReactNode
}

export default function ShinyButton({
  children,
  type = 'button', // Default to "button" type
  ...rest
}: ShinyButtonProps) {
  return (
    <motion.button
      type={type}
      initial={{ '--x': '100%', scale: 1 }}
      animate={{ '--x': '-100%' }}
      whileTap={{ scale: 0.88 }}
      transition={{
        repeat: Infinity,
        repeatType: 'loop',
        repeatDelay: 1,
        type: 'spring',
        stiffness: 20,
        damping: 15,
        mass: 2,
        scale: {
          type: 'spring',
          stiffness: 10,
          damping: 5,
          mass: 0.1,
        },
      }}
      className="radial-gradient relative w-40 rounded-md px-6 py-2"
      {...rest}
    >
      <span className="linear-mask relative block h-full w-full tracking-wide">
        {children}
      </span>
      <span className="linear-overlay absolute inset-0 block rounded-md p-px" />
    </motion.button>
  )
}
Susmita-Dey commented 2 months ago

@estebanarriaga I used your code to fix the issue but didn't work. @mazwrld Can you share the full code for this component?

@sixfwa I'd request you to put up a solution for this in the comments of YouTube channel.

mazwrld commented 1 month ago

@Susmita-Dey here it is.


'use client' // for nextjs 

import { motion, type HTMLMotionProps } from 'framer-motion'

type ExtendedMotionProps = HTMLMotionProps<'button'> & {
  initial?: Record<string, unknown>
  animate?: Record<string, unknown>
}

type ShinyButtonProps = ExtendedMotionProps & {
  children: React.ReactNode
}

export default function ShinyButton({
  children,
  type = 'button', // Default to "button" type
  ...rest
}: ShinyButtonProps) {
  return (
    <motion.button
      type={type}
      initial={{ '--x': '100%', scale: 1 }}
      animate={{ '--x': '-100%' }}
      whileTap={{ scale: 0.88 }}
      transition={{
        repeat: Infinity,
        repeatType: 'loop',
        repeatDelay: 1,
        type: 'spring',
        stiffness: 20,
        damping: 15,
        mass: 2,
        scale: {
          type: 'spring',
          stiffness: 10,
          damping: 5,
          mass: 0.1,
        },
      }}
      className="radial-gradient relative w-40 rounded-md px-6 py-2"
      {...rest}
    >
      <span className="linear-mask relative block h-full w-full tracking-wide">
        {children}
      </span>
      <span className="linear-overlay absolute inset-0 block rounded-md p-px" />
    </motion.button>
  )
}
Susmita-Dey commented 1 month ago

@mazwrld Thanks for sharing this. The button is being rendered now but there's issue with the button style and not showing up as shown in the video. Can you share your repo link?