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
Basic 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 { FormView, FormLayout, TextField, Next, Back } from "./components";
import { Controller } from "./controller";
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 }) => (
<Controller
step="name"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
name: z.string(),
}),
)}
>
<FormLayout
heading="What is your name?"
description="We would like to know what is your name"
fields={[<TextField key="name" name="name" label="Name" />]}
button={<Next>Next</Next>}
/>
</FormView>
</Controller>
),
},
},
{
form: {
values: () => ({
surname: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<Controller
step="surname"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
surname: z.string(),
}),
)}
>
<FormLayout
heading="What is your surname?"
description="We would like to know what is your surname"
fields={[
<TextField key="surname" name="surname" label="Surname" />,
]}
button={<Next>Next</Next>}
back={<Back />}
/>
</FormView>
</Controller>
),
},
},
{
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 controller/controller.tsx
file so that it contains the following:
// controller/controller.tsx
import type { ReactNode } from "react";
import type { OnNext, OnBack, GetState, SetState } from "@formity/react";
import { useMemo } from "react";
import { AnimatePresence, motion } from "motion/react";
import { ControllerContext } from "./controller-context";
interface ControllerProps {
step: string;
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
children: ReactNode;
}
export function Controller({
step,
onNext,
onBack,
getState,
setState,
children,
}: ControllerProps) {
const values = useMemo(
() => ({
onNext,
onBack,
getState,
setState,
}),
[onNext, onBack, getState, setState],
);
return (
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={step}
initial={{ opacity: 0, x: 100 }}
animate={{
x: 0,
opacity: 1,
transition: { delay: 0.25, duration: 0.25 },
}}
exit={{
x: -100,
opacity: 0,
transition: { delay: 0, duration: 0.25 },
}}
className="h-full"
>
<ControllerContext.Provider value={values}>
{children}
</ControllerContext.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.
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:
// controller/controller.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 { ControllerContext } from "./controller-context";
interface ControllerProps {
step: string;
onNext: OnNext;
onBack: OnBack;
getState: GetState;
setState: SetState;
children: ReactNode;
}
export function Controller({
step,
onNext,
onBack,
getState,
setState,
children,
}: ControllerProps) {
const [animate, setAnimate] = useState<"none" | "next" | "back">("none");
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("none")}
>
<motion.div
key={step}
initial={{ opacity: 0, x: 100 }}
animate={{
x: 0,
opacity: 1,
transition: { delay: 0.25, duration: 0.25 },
}}
{...motionProps(animate)}
className="h-full"
>
<ControllerContext.Provider value={values}>
{children}
</ControllerContext.Provider>
</motion.div>
</AnimatePresence>
);
}
function motionProps(animate: "none" | "next" | "back"): MotionProps {
if (animate === "next") {
return {
initial: { x: 100, opacity: 0 },
exit: {
x: -100,
opacity: 0,
transition: { delay: 0, duration: 0.25 },
},
};
}
if (animate === "back") {
return {
initial: { x: -100, opacity: 0 },
exit: {
x: 100,
opacity: 0,
transition: { delay: 0, duration: 0.25 },
},
};
}
return {};
}
We've created a state to control the animation that should be performed. Additionally, the functions to navigate between steps are called using setTimeout
. This ensures the animation state is updated beforehand, allowing the correct animation to run smoothly.
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,
FormView,
FormLayout,
TextField,
Next,
Back,
} from "./components";
import { Controller } from "./controller";
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 }}>
<Controller
step="name"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
name: z.string(),
}),
)}
>
<FormLayout
heading="What is your name?"
description="We would like to know what is your name"
fields={[<TextField key="name" name="name" label="Name" />]}
button={<Next>Next</Next>}
/>
</FormView>
</Controller>
</Screen>
),
},
},
{
form: {
values: () => ({
surname: ["", []],
}),
render: ({ values, onNext, onBack, getState, setState }) => (
<Screen progress={{ total: 2, current: 2 }}>
<Controller
step="surname"
onNext={onNext}
onBack={onBack}
getState={getState}
setState={setState}
>
<FormView
defaultValues={values}
resolver={zodResolver(
z.object({
surname: z.string(),
}),
)}
>
<FormLayout
heading="What is your surname?"
description="We would like to know what is your surname"
fields={[
<TextField key="surname" name="surname" label="Surname" />,
]}
button={<Next>Next</Next>}
back={<Back />}
/>
</FormView>
</Controller>
</Screen>
),
},
},
{
return: ({ name, surname }) => ({
name,
surname,
}),
},
];