Citizen
Citizen

Reputation: 12947

How can I add a fade-in animation for Nextjs/Image when it loads?

I'm using next/image, which works great, except the actual image loading in is super jarring and there's no animation or fade in. Is there a way to accomplish this? I've tried a ton of things and none of them work.

Here's my code:

<Image
  src={source}
  alt=""
  layout="responsive"
  width={750}
  height={height}
  className="bg-gray-400"
  loading="eager"
/>

According to the docs I can use the className prop, but those are loaded immediately and I can't figure out any way to apply a class after it's loaded.

I also tried onLoad, and according to this ticket, it isn't supported: https://github.com/vercel/next.js/issues/20368

Upvotes: 12

Views: 27780

Answers (5)

k2fx
k2fx

Reputation: 1271

   <Image 
       // ...other props
       className="img img--hidden"
       onLoadingComplete={(image)=>image.classList.remove("img--hidden")}
     />

CSS:

.img {
  opacity: 1;
  transition: opacity 3s;
}
.img--hidden {
  opacity: 0;
}

Upvotes: 1

Gabriel Linassi
Gabriel Linassi

Reputation: 551

NextJS now supports placeholder. You can fill the blurDataURL property with the base64 string of the image which you can easily get using the lib plaiceholder on getServerSideProps or getStaticProps. Then to make the transition smoothly you can add transition: 0.3s;

Quick sample:

export const UserInfo: React.FC<TUserInfo> = ({ profile }) => {
  return (
    <div className="w-24 h-24 rounded-full overflow-hidden">
      <Image
        src={profile.image}
        placeholder="blur"
        blurDataURL={profile.blurDataURL}
        width="100%"
        height="100%"
      />
    </div>
  );
};

export async function getServerSideProps(props: any) {
  const { username } = props.query;

  const userProfileByName = `${BASE_URL}/account/user_profile_by_user_name?user_name=${username}`;
  const profileResponse = await (await fetch(userProfileByName)).json();
  const profile = profileResponse?.result?.data[0];

  const { base64 } = await getPlaiceholder(profile.profile_image);

  return {
    props: {
      profile: {
        ...profile,
        blurDataURL: base64,
      },
    },
  };
}

index.css

img {
  transition: 0.3s;
}

======== EDIT ==========

If you have the image in the public folder for ex, you don't need to do the above steps, just statically import the asset and add the placeholder type. NextJS will do the rest. Also, make sure to make good use of the size property to load the correct image size for the viewport and use the priority prop for above-the-fold assets. Example:

import NextImage from 'next/image'
import imgSrc from '/public/imgs/awesome-img.png'

return (
  ...
  <NextImage 
    src={imgSrc}
    placeholder='blur'
    priority
    layout="fill"
    sizes="(min-width: 1200px) 33vw, (min-width: 768px) 50vw, 100vw"
  />
)

Upvotes: 10

Liam McKenna
Liam McKenna

Reputation: 11

Yes, its possible to capture the event where the actual image loads. I found an answer to this on Reddit and wanted to repost it here for others like me searching for an anwser.

"To get onLoad to work in the NextJS image component you need make sure it's not the 1x1 px they use as placeholder that is the target.

const [imageIsLoaded, setImageIsLoaded] = useState(false)  
<Image
    width={100}
    height={100}
    src={'some/src.jpg'}
    onLoad={event => {
        const target = event.target;

        // next/image use an 1x1 px git as placeholder. We only want the onLoad event on the actual image
        if (target.src.indexOf('data:image/gif;base64') < 0) {
            setImageIsLoaded(true)
        }
    }}
/>

From there you can just use the imageIsLoaded boolean to do some fadein with something like the Framer Motion library.

Source: https://www.reddit.com/r/nextjs/comments/lwx0j0/fade_in_when_loading_nextimage/

Upvotes: 0

Mika
Mika

Reputation: 642

I wanted to achieve the same thing and tried to use the onLoad event, therefore. The Image component of nextJs accepts this as prop, so this was my result:

const animationVariants = {
    visible: { opacity: 1 },
    hidden: { opacity: 0 },
}

const FadeInImage = props => {
    const [loaded, setLoaded] = useState(false);
    const animationControls = useAnimation();
    useEffect(
        () => {
            if(loaded){
                animationControls.start("visible");
            }
        },
        [loaded]
    );
    return(
        <motion.div
            initial={"hidden"}
            animate={animationControls}
            variants={animationVariants}
            transition={{ ease: "easeOut", duration: 1 }}
        >
            <Image
                {...p}
                onLoad={() => setLoaded(true)}
            />
        </motion.div>
    );
}

However, the Image does not always fade-in, the onLoad event seems to be triggered too early if the image is not cached already. I suspect this is a bug that will be fixed in future nextJS releases. If someone else finds a solution, please keep me updated!

The solution above however works often, and since onLoad gets triggered every time, it does not break anything.

Edit: This solution uses framer-motion for the animation. This could also be replaced by any other animation library or native CSS transitions

Upvotes: 3

oldo.nicho
oldo.nicho

Reputation: 2289

You could try use next-placeholder to achieve this sort of effect

Upvotes: 1

Related Questions