Advanced concepts
Animations
Learn how to add animations in these forms by using Motion.
First steps
To add animations in these forms we can use the Motion package. To learn how to do it, clone the following Github repository so that you don't need to start from scratch.
git clone https://github.com/martiserra99/formity-react-docs
Make sure you run the following command to install all the dependencies:
npm install
Additionally, you also need to install Motion by doing the following:
npm install motion
Create form
We will start by creating a very basic form without using animations. For that, we will create a schema.tsx
file with the following content:
// schema.tsx
import type { Schema, Form, Return } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Step, Layout, TextField, NextButton, BackButton } from "./components";
import { MultiStep } from "./multi-step";
export type Values = [
Form<{ name: string }>,
Form<{ surname: string }>,
Return<{ name: string; surname: string }>,
];
export const schema: Schema<Values> = [
{
form: {
values: () => ({
name: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
step="name"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={values}
resolver={zodResolver(
z.object({
name: z.string(),
}),
)}
>
<Layout
heading="What is your name?"
description="We would like to know what is your name"
fields={[<TextField key="name" name="name" label="Name" />]}
button={<NextButton>Next</NextButton>}
/>
</Step>
</MultiStep>
),
},
},
{
form: {
values: () => ({
surname: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<MultiStep
step="surname"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={values}
resolver={zodResolver(
z.object({
surname: z.string(),
}),
)}
>
<Layout
heading="What is your surname?"
description="We would like to know what is your surname"
fields={[
<TextField key="surname" name="surname" label="Surname" />,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
),
},
},
{
return: ({ name, surname }) => ({
name,
surname,
}),
},
];
Then, we will update the App.tsx
file so that it contains the following:
// App.tsx
import { useCallback, useState } from "react";
import { Formity, OnReturn, ReturnValues } from "@formity/react";
import { Data } from "./components";
import { schema, Values } from "./schema";
export default function App() {
const [values, setValues] = useState<ReturnValues<Values> | null>(null);
const onReturn = useCallback<OnReturn<Values>>((values) => {
setValues(values);
}, []);
if (values) {
return <Data data={values} onStart={() => setValues(null)} />;
}
return <Formity<Values> schema={schema} onReturn={onReturn} />;
}
Animate form
To animate form transitions, we will update the multi-step/multi-step.tsx
file so that it contains the following:
// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, GetState, SetState } from "@formity/react";
import { useCallback, useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { MultiStepContext } from "./multi-step-context";
interface MultiStepProps {
step: string;
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
children: ReactNode;
}
export function MultiStep({
step,
onNext,
onBack,
getState,
setState,
children,
}: MultiStepProps) {
const [animate, setAnimate] = useState<boolean>(false);
const handleNext = useCallback<OnNext>(
(values) => {
setAnimate(true);
setTimeout(() => onNext(values), 0);
},
[onNext],
);
const handleBack = useCallback<OnBack>(
(values) => {
setAnimate(true);
setTimeout(() => onBack(values), 0);
},
[onBack],
);
const values = useMemo(
() => ({ onNext: handleNext, onBack: handleBack, getState, setState }),
[handleNext, handleBack, getState, setState],
);
return (
<AnimatePresence
mode="popLayout"
initial={false}
onExitComplete={() => setAnimate(false)}
>
<motion.div
key={step}
inert={animate}
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { delay: 0.25, duration: 0.25 },
}}
exit={{
opacity: 0,
transition: { delay: 0, duration: 0.25 },
}}
className="h-full"
>
<MultiStepContext.Provider value={values}>
{children}
</MultiStepContext.Provider>
</motion.div>
</AnimatePresence>
);
}
Here, we are using the AnimatePresence
component to handle animations. It ensures that whenever the key
prop changes, the corresponding component transitions smoothly, creating a more dynamic user experience.
We've also implemented a state to determine when an animation is in progress. During this time, navigation between steps is disabled by using the inert property. We could have also disabled the buttons instead.
Finally, the setTimeout
function is used to guarantee that the animation state updates before moving on to the next step or previous step.
Different animations
We may want to use different animations depending on whether we are going to the next or previous step. To achieve it, we will need to update the file the following way:
// multi-step/multi-step.tsx
import type { ReactNode } from "react";
import type { MotionProps } from "motion/react";
import type { OnNext, OnBack, GetState, SetState } from "@formity/react";
import { useCallback, useMemo, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import { MultiStepContext } from "./multi-step-context";
interface MultiStepProps {
step: string;
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
children: ReactNode;
}
export function MultiStep({
step,
onNext,
onBack,
getState,
setState,
children,
}: MultiStepProps) {
const [animate, setAnimate] = useState<"next" | "back" | false>(false);
const handleNext = useCallback<OnNext>(
(values) => {
setAnimate("next");
setTimeout(() => onNext(values), 0);
},
[onNext],
);
const handleBack = useCallback<OnBack>(
(values) => {
setAnimate("back");
setTimeout(() => onBack(values), 0);
},
[onBack],
);
const values = useMemo(
() => ({ onNext: handleNext, onBack: handleBack, getState, setState }),
[handleNext, handleBack, getState, setState],
);
return (
<AnimatePresence
mode="popLayout"
initial={false}
onExitComplete={() => setAnimate(false)}
>
<motion.div
key={step}
inert={Boolean(animate)}
animate={{
x: 0,
opacity: 1,
transition: { delay: 0.25, duration: 0.25 },
}}
{...motionProps(animate)}
className="h-full"
>
<MultiStepContext.Provider value={values}>
{children}
</MultiStepContext.Provider>
</motion.div>
</AnimatePresence>
);
}
function motionProps(animate: "next" | "back" | false): MotionProps {
switch (animate) {
case "next":
return {
initial: { x: 100, opacity: 0 },
exit: {
x: -100,
opacity: 0,
transition: { delay: 0, duration: 0.25 },
},
};
case "back":
return {
initial: { x: -100, opacity: 0 },
exit: {
x: 100,
opacity: 0,
transition: { delay: 0, duration: 0.25 },
},
};
default:
return {};
}
}
We've updated the state to track whether we're moving to the next or previous step and adjusted the animation props accordingly.
Progress bar
We could also add a progress bar that is animated every time we go to a different step. To do it, we will create a components/screen.tsx
file with the following component:
// components/screen.tsx
import type { ReactNode } from "react";
import { motion } from "framer-motion";
interface ScreenProps {
progress: { total: number; current: number };
children: ReactNode;
}
export default function Screen({ progress, children }: ScreenProps) {
return (
<div className="relative h-full w-full">
<Progress total={progress.total} current={progress.current} />
{children}
</div>
);
}
interface ProgressProps {
total: number;
current: number;
}
function Progress({ total, current }: ProgressProps) {
return (
<div className="absolute left-0 right-0 top-0 h-1 bg-indigo-500/50">
<motion.div
className="h-full bg-indigo-500"
animate={{ width: `${(current / total) * 100}%` }}
initial={false}
/>
</div>
);
}
We will need to export this component from the components/index.ts
file:
// components/index.ts
export { default as Screen } from "@/components/screen";
// ...
Finally, we will be able to use this component in the schema:
// schema.tsx
import type { Schema, Form, Return } from "@formity/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Screen,
Step,
Layout,
TextField,
NextButton,
BackButton,
} from "./components";
import { MultiStep } from "./multi-step";
export type Values = [
Form<{ name: string }>,
Form<{ surname: string }>,
Return<{ name: string; surname: string }>,
];
export const schema: Schema<Values> = [
{
form: {
values: () => ({
name: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<Screen progress={{ total: 2, current: 1 }}>
<MultiStep
step="name"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={values}
resolver={zodResolver(
z.object({
name: z.string(),
}),
)}
>
<Layout
heading="What is your name?"
description="We would like to know what is your name"
fields={[<TextField key="name" name="name" label="Name" />]}
button={<NextButton>Next</NextButton>}
/>
</Step>
</MultiStep>
</Screen>
),
},
},
{
form: {
values: () => ({
surname: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<Screen progress={{ total: 2, current: 2 }}>
<MultiStep
step="surname"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<Step
defaultValues={values}
resolver={zodResolver(
z.object({
surname: z.string(),
}),
)}
>
<Layout
heading="What is your surname?"
description="We would like to know what is your surname"
fields={[
<TextField key="surname" name="surname" label="Surname" />,
]}
button={<NextButton>Next</NextButton>}
back={<BackButton />}
/>
</Step>
</MultiStep>
</Screen>
),
},
},
{
return: ({ name, surname }) => ({
name,
surname,
}),
},
];