Today’s post is actually somewhat of a fun one! No frustrating broken things, no shenanigans with a black box to worry about. Just some simple Typescript fun.
So let’s take this common hero component. It has some gorgeous animations courtesy of Framer Motion. Highly recommend this library for how simple it is to get going and the sheer flexibility of the animations. Take your div and turn it into a motion.div. Apply the right attributes. Done. I did mention it was easy, right? Anyway, check out this component:
import { JSX } from 'react';
import { motion } from 'framer-motion';
import styles from './PageHero.module.css';
import {
ComponentParams,
ImageField,
Text,
TextField,
withDatasourceCheck,
} from '@sitecore-jss/sitecore-jss-nextjs';
interface HeroProps {
rendering: {
componentName: string;
dataSource: string;
};
params: ComponentParams;
fields: {
PersonaImage: ImageField;
Title: TextField;
Description: TextField;
};
}
const PageHero = (props: HeroProps): JSX.Element => {
return (
<div>
<div className={styles['page-hero']}>
<motion.div
className={styles['persona-image-container']}
initial={{ opacity: 0, x: -250 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: 'easeInOut' }}
></motion.div>
<div className={styles['content-container']}>
<motion.h1
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: 'easeInOut', delay: 0.7 }}
>
{props?.fields?.Title?.value && (
<Text field={props?.fields?.Title} />
)}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: 'easeInOut', delay: 1.4 }}
>
{props?.fields?.Description?.value && (
<Text field={props?.fields?.Description} />
)}
</motion.p>
</div>
</div>
</div>
);
};
export default withDatasourceCheck()<HeroProps>(PageHero);
Essentially there’s three motions here. One for the background (first div), one for the h1, one for the paragraph tag with the description in it. The delay attribute gives it a bit of a pizazz. But content authors aren’t really interested in pizazz. Plus, we don’t want any random animations to break the hydration in Experience Editor. Now, I could do a whole “If in editing, show this markup (div), else show the animation markup (motion.div). This is going to almost double the size of our files though. So what do we do? We’ll use something called the spread syntax.
But for serious, what does the Spread Syntax do? It takes an iterable (not to be confused with an edible) and expands it into elements. How does THIS help us? Well, let’s take a look at our first motion.div:
<motion.div
className={styles['persona-image-container']}
initial={{ opacity: 0, x: -250 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: 'easeInOut' }}
></motion.div>
I can actually extract all those animation attributes out into an object:
const bgAnimationAttributes = {
initial: { opacity: 0, x: -250 },
whileInView: { opacity: 1, x: 0 },
viewport: { once: true },
transition: { duration: 0.7, ease: 'easeInOut' },
};
And then I’d need to update my div to the spread syntax:
<motion.div
className={styles['persona-image-container']}
{...bgAnimationAttributes}
></motion.div>
When this executes, those attributes will get expanded into the motion.div. But, we’re not done yet. We’re still need to disable this in Experience Editor. We’ll be using our trusty useSitecoreContext (to check what mode we’re in) coupled with a ternary operator.
const bgAnimationAttributes = !useSitecoreContext().sitecoreContext.pageEditing
? {
initial: { opacity: 0, x: -250 },
whileInView: { opacity: 1, x: 0 },
viewport: { once: true },
transition: { duration: 0.7, ease: 'easeInOut' },
}
: {};
That basically says “If I’m in not Page Editing mode, use these attributes, else no attributes” which would effectively disable our animation in EE. Here’s the final file:
import { JSX } from 'react';
import { motion } from 'framer-motion';
import styles from './PageHero.module.css';
import {
ComponentParams,
ImageField,
Text,
TextField,
useSitecoreContext,
withDatasourceCheck,
} from '@sitecore-jss/sitecore-jss-nextjs';
interface HeroProps {
rendering: {
componentName: string;
dataSource: string;
};
params: ComponentParams;
fields: {
PersonaImage: ImageField;
Title: TextField;
Description: TextField;
};
}
const PageHero = (props: HeroProps): JSX.Element => {
const isEditing = !useSitecoreContext().sitecoreContext.pageEditing;
const bgAnimationAttributes = !isEditing
? {
initial: { opacity: 0, x: -250 },
whileInView: { opacity: 1, x: 0 },
viewport: { once: true },
transition: { duration: 0.7, ease: 'easeInOut' },
}
: {};
const h1AnimationAttributes = !isEditing
? {
initial: { opacity: 0, y: 50 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
transition: { duration: 0.7, ease: 'easeInOut', delay: 0.7 },
}
: {};
const descriptionAnimationAttributes = !isEditing
? {
initial: { opacity: 0, y: 50 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
transition: { duration: 0.7, ease: 'easeInOut', delay: 1.4 },
}
: {};
return (
<div>
<div className={styles['page-hero']}>
<motion.div
className={styles['persona-image-container']}
{...bgAnimationAttributes}
></motion.div>
<div className={styles['content-container']}>
<motion.h1 {...h1AnimationAttributes}>
{props?.fields?.Title?.value && (
<Text field={props?.fields?.Title} />
)}
</motion.h1>
<motion.p {...descriptionAnimationAttributes}>
{props?.fields?.Description?.value && (
<Text field={props?.fields?.Description} />
)}
</motion.p>
</div>
</div>
</div>
);
};
export default withDatasourceCheck()<HeroProps>(PageHero);
I love the simplicity of this. You’re not messing with the actual markup, nor introducing any duplication.