Components
Animated Form
Animated Form
Animated form mimicking account creation with progressive name/password animation and completion checkmarks.
Create Account
Alex Morgan
********
Installation
1
Install the packages
npm i motion react-icons clsx tailwind-merge
2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
animated-form.tsx
"use client";
import { IoMdCheckmark } from "react-icons/io";
import { FaGithub } from "react-icons/fa6";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type AnimatedFormProps = {
delay?: number;
name?: string;
};
const AnimatedForm = ({
delay = 7000,
name = "Alex Morgan",
}: AnimatedFormProps) => {
const [animationKey, setAnimationKey] = useState(0);
const delayTime = Math.max(delay, 7000);
useEffect(() => {
const interval = setInterval(() => {
setAnimationKey((prev) => prev + 1);
}, delayTime);
return () => clearInterval(interval);
}, [delayTime]);
return <Animatedform key={animationKey} name={name} />;
};
export default AnimatedForm;
const Animatedform = ({ name }: { name: string }) => {
const password = "********";
const circleLength = 2 * Math.PI * 50;
const nameAnimationDuration = Math.ceil(name.length / 5);
const passwordAnimationDuration = 2;
const nameStaggerDelay = nameAnimationDuration / name.length;
const passwordStaggerDelay = passwordAnimationDuration / password.length;
return (
<div className={cn("relative", "w-full max-w-[340px]")}>
<div className="w-full rounded-[12px] border border-neutral-200/70 p-1.5 dark:border-neutral-900/70">
<div className="relative flex flex-col gap-1 divide-y divide-neutral-200 rounded-lg border border-neutral-200 dark:divide-neutral-900 dark:border-neutral-900">
<div className="bg-gradient-to-r from-neutral-700 to-neutral-300 bg-clip-text px-3 pb-2 pt-3 text-[14px] leading-[1rem] tracking-wide text-transparent dark:from-neutral-400 dark:to-neutral-700">
Create Account
</div>
<div className="flex flex-col gap-2 p-2">
<div className="flex w-full items-center justify-between gap-4 rounded-md border bg-gradient-to-r from-neutral-50 to-neutral-100 p-2 dark:from-neutral-900 dark:to-neutral-950">
<div className="text-xs">
{name.split("").map((char, index) => (
<motion.span
key={`name-${index}`}
className="inline-block font-[350]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.1,
delay: index * nameStaggerDelay,
ease: "easeOut",
}}
>
{char === " " ? " " : char}
</motion.span>
))}
</div>
<div className="relative">
<svg width="20" height="20" className="rotate-[-90deg]">
<motion.circle
cx="10"
cy="10"
r="7"
stroke="#22c55e"
strokeWidth="2"
fill="transparent"
strokeDasharray={circleLength}
strokeDashoffset={circleLength}
animate={{ strokeDashoffset: 0 }}
transition={{
duration: nameAnimationDuration * 3 + 1,
ease: "easeInOut",
delay: 0,
}}
/>
<motion.circle
cx="10"
cy="10"
r="7"
fill="#22c55e"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
delay: nameAnimationDuration + 0.1,
}}
/>
</svg>
<motion.div
className="absolute inset-0 flex items-center justify-center text-background"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.2,
delay: nameAnimationDuration + 0.2,
}}
>
<IoMdCheckmark className="size-2.5" />
</motion.div>
</div>
</div>
<div className="flex items-center justify-between gap-8 rounded-md border bg-background bg-gradient-to-r from-neutral-50 to-neutral-100 p-2 dark:from-neutral-900 dark:to-neutral-950">
<div className="font-mono text-xs">
{password.split("").map((char, index) => (
<motion.span
key={`password-${index}`}
className="inline-block"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.1,
delay:
nameAnimationDuration +
0.5 +
index * passwordStaggerDelay,
ease: "easeOut",
}}
>
{char}
</motion.span>
))}
</div>
<div className="relative">
<svg width="20" height="20" className="rotate-[-90deg]">
<motion.circle
cx="10"
cy="10"
r="7"
stroke="#22c55e"
strokeWidth="2"
fill="transparent"
strokeDasharray={circleLength}
strokeDashoffset={circleLength}
animate={{ strokeDashoffset: 0 }}
transition={{
duration: 7,
ease: "easeInOut",
delay: nameAnimationDuration + 0.5,
}}
/>
<motion.circle
cx="10"
cy="10"
r="7"
fill="#22c55e"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
delay:
nameAnimationDuration + passwordAnimationDuration + 0.6,
}}
/>
</svg>
<motion.div
className="absolute inset-0 flex items-center justify-center text-background"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.2,
delay:
nameAnimationDuration + passwordAnimationDuration + 0.7,
}}
>
<IoMdCheckmark className="size-2.5" />
</motion.div>
</div>
</div>
<div className="h-[37.6px] rounded-md border bg-gradient-to-r from-neutral-50 to-neutral-100 opacity-40 dark:from-neutral-900 dark:to-neutral-950" />
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 h-[40px] w-full [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 left-0 h-[100px] w-[12px] [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 right-0 h-[100px] w-[12px] [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 left-0 flex h-[50px] w-full items-center justify-around px-6">
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<FaGithub className="size-6 text-primary" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<ShopifyIcon className="size-6" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<TwitchIcon className="size-6" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<YoutubeIcon className="size-6" />
</div>
</div>
</div>
);
};
export const ShopifyIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="256"
height="292"
viewBox="0 0 256 292"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
fill="currentColor"
{...props}
>
<path
d="M223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-1.703-1.703-5.029-1.185-6.32-.805-.19.056-3.388 1.043-8.678 2.68-5.18-14.906-14.322-28.604-30.405-28.604-.444 0-.901.018-1.358.044C129.31 3.407 123.644.779 118.75.779c-37.465 0-55.364 46.835-60.976 70.635-14.558 4.511-24.9 7.718-26.221 8.133-8.126 2.549-8.383 2.805-9.45 10.462C21.3 95.806.038 260.235.038 260.235l165.678 31.042 89.77-19.42S223.973 58.8 223.775 57.34zM156.49 40.848l-14.019 4.339c.005-.988.01-1.96.01-3.023 0-9.264-1.286-16.723-3.349-22.636 8.287 1.04 13.806 10.469 17.358 21.32zm-27.638-19.483c2.304 5.773 3.802 14.058 3.802 25.238 0 .572-.005 1.095-.01 1.624-9.117 2.824-19.024 5.89-28.953 8.966 5.575-21.516 16.025-31.908 25.161-35.828zm-11.131-10.537c1.617 0 3.246.549 4.805 1.622-12.007 5.65-24.877 19.88-30.312 48.297l-22.886 7.088C75.694 46.16 90.81 10.828 117.72 10.828z"
fill="#95BF46"
/>
<path
d="M221.237 54.983c-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-.637-.634-1.496-.959-2.394-1.099l-12.527 256.233 89.762-19.418S223.972 58.8 223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357"
fill="#5E8E3E"
/>
<path
d="M135.242 104.585l-11.069 32.926s-9.698-5.176-21.586-5.176c-17.428 0-18.305 10.937-18.305 13.693 0 15.038 39.2 20.8 39.2 56.024 0 27.713-17.577 45.558-41.277 45.558-28.44 0-42.984-17.7-42.984-17.7l7.615-25.16s14.95 12.835 27.565 12.835c8.243 0 11.596-6.49 11.596-11.232 0-19.616-32.16-20.491-32.16-52.724 0-27.129 19.472-53.382 58.778-53.382 15.145 0 22.627 4.338 22.627 4.338"
fill="#FFF"
/>
</svg>
);
export const TwitchIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2400 2800"
xmlSpace="preserve"
fill="currentColor"
{...props}
>
<path fill="#fff" d="m2200 1300-400 400h-400l-350 350v-350H600V200h1600z" />
<g fill="#9146ff">
<path d="M500 0 0 500v1800h600v500l500-500h400l900-900V0H500zm1700 1300-400 400h-400l-350 350v-350H600V200h1600v1100z" />
<path d="M1700 550h200v600h-200zm-550 0h200v600h-200z" />
</g>
</svg>
);
export const YoutubeIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 180"
width={256}
height={180}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
fill="currentColor"
{...props}
>
<path
d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134Z"
fill="red"
/>
<path fill="#FFF" d="m102.421 128.06 66.328-38.418-66.328-38.418z" />
</svg>
);
"use client";
import { IoMdCheckmark } from "react-icons/io";
import { FaGithub } from "react-icons/fa6";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type AnimatedFormProps = {
delay?: number;
name?: string;
};
const AnimatedForm = ({
delay = 7000,
name = "Alex Morgan",
}: AnimatedFormProps) => {
const [animationKey, setAnimationKey] = useState(0);
const delayTime = Math.max(delay, 7000);
useEffect(() => {
const interval = setInterval(() => {
setAnimationKey((prev) => prev + 1);
}, delayTime);
return () => clearInterval(interval);
}, [delayTime]);
return <Animatedform key={animationKey} name={name} />;
};
export default AnimatedForm;
const Animatedform = ({ name }: { name: string }) => {
const password = "********";
const circleLength = 2 * Math.PI * 50;
const nameAnimationDuration = Math.ceil(name.length / 5);
const passwordAnimationDuration = 2;
const nameStaggerDelay = nameAnimationDuration / name.length;
const passwordStaggerDelay = passwordAnimationDuration / password.length;
return (
<div className={cn("relative", "w-full max-w-[340px]")}>
<div className="w-full rounded-[12px] border border-neutral-200/70 p-1.5 dark:border-neutral-900/70">
<div className="relative flex flex-col gap-1 divide-y divide-neutral-200 rounded-lg border border-neutral-200 dark:divide-neutral-900 dark:border-neutral-900">
<div className="bg-gradient-to-r from-neutral-700 to-neutral-300 bg-clip-text px-3 pb-2 pt-3 text-[14px] leading-[1rem] tracking-wide text-transparent dark:from-neutral-400 dark:to-neutral-700">
Create Account
</div>
<div className="flex flex-col gap-2 p-2">
<div className="flex w-full items-center justify-between gap-4 rounded-md border bg-gradient-to-r from-neutral-50 to-neutral-100 p-2 dark:from-neutral-900 dark:to-neutral-950">
<div className="text-xs">
{name.split("").map((char, index) => (
<motion.span
key={`name-${index}`}
className="inline-block font-[350]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.1,
delay: index * nameStaggerDelay,
ease: "easeOut",
}}
>
{char === " " ? " " : char}
</motion.span>
))}
</div>
<div className="relative">
<svg width="20" height="20" className="rotate-[-90deg]">
<motion.circle
cx="10"
cy="10"
r="7"
stroke="#22c55e"
strokeWidth="2"
fill="transparent"
strokeDasharray={circleLength}
strokeDashoffset={circleLength}
animate={{ strokeDashoffset: 0 }}
transition={{
duration: nameAnimationDuration * 3 + 1,
ease: "easeInOut",
delay: 0,
}}
/>
<motion.circle
cx="10"
cy="10"
r="7"
fill="#22c55e"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
delay: nameAnimationDuration + 0.1,
}}
/>
</svg>
<motion.div
className="absolute inset-0 flex items-center justify-center text-background"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.2,
delay: nameAnimationDuration + 0.2,
}}
>
<IoMdCheckmark className="size-2.5" />
</motion.div>
</div>
</div>
<div className="flex items-center justify-between gap-8 rounded-md border bg-background bg-gradient-to-r from-neutral-50 to-neutral-100 p-2 dark:from-neutral-900 dark:to-neutral-950">
<div className="font-mono text-xs">
{password.split("").map((char, index) => (
<motion.span
key={`password-${index}`}
className="inline-block"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.1,
delay:
nameAnimationDuration +
0.5 +
index * passwordStaggerDelay,
ease: "easeOut",
}}
>
{char}
</motion.span>
))}
</div>
<div className="relative">
<svg width="20" height="20" className="rotate-[-90deg]">
<motion.circle
cx="10"
cy="10"
r="7"
stroke="#22c55e"
strokeWidth="2"
fill="transparent"
strokeDasharray={circleLength}
strokeDashoffset={circleLength}
animate={{ strokeDashoffset: 0 }}
transition={{
duration: 7,
ease: "easeInOut",
delay: nameAnimationDuration + 0.5,
}}
/>
<motion.circle
cx="10"
cy="10"
r="7"
fill="#22c55e"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
delay:
nameAnimationDuration + passwordAnimationDuration + 0.6,
}}
/>
</svg>
<motion.div
className="absolute inset-0 flex items-center justify-center text-background"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.2,
delay:
nameAnimationDuration + passwordAnimationDuration + 0.7,
}}
>
<IoMdCheckmark className="size-2.5" />
</motion.div>
</div>
</div>
<div className="h-[37.6px] rounded-md border bg-gradient-to-r from-neutral-50 to-neutral-100 opacity-40 dark:from-neutral-900 dark:to-neutral-950" />
</div>
</div>
</div>
<div className="absolute bottom-0 left-0 h-[40px] w-full [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 left-0 h-[100px] w-[12px] [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 right-0 h-[100px] w-[12px] [background-image:linear-gradient(to_top,theme(colors.background)_60%,transparent_100%)]" />
<div className="absolute bottom-0 left-0 flex h-[50px] w-full items-center justify-around px-6">
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<FaGithub className="size-6 text-primary" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<ShopifyIcon className="size-6" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<TwitchIcon className="size-6" />
</div>
<div className="rounded-full bg-gradient-to-b from-neutral-300 to-neutral-100 p-2 dark:from-neutral-700 dark:to-neutral-900">
<YoutubeIcon className="size-6" />
</div>
</div>
</div>
);
};
export const ShopifyIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="256"
height="292"
viewBox="0 0 256 292"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
fill="currentColor"
{...props}
>
<path
d="M223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-1.703-1.703-5.029-1.185-6.32-.805-.19.056-3.388 1.043-8.678 2.68-5.18-14.906-14.322-28.604-30.405-28.604-.444 0-.901.018-1.358.044C129.31 3.407 123.644.779 118.75.779c-37.465 0-55.364 46.835-60.976 70.635-14.558 4.511-24.9 7.718-26.221 8.133-8.126 2.549-8.383 2.805-9.45 10.462C21.3 95.806.038 260.235.038 260.235l165.678 31.042 89.77-19.42S223.973 58.8 223.775 57.34zM156.49 40.848l-14.019 4.339c.005-.988.01-1.96.01-3.023 0-9.264-1.286-16.723-3.349-22.636 8.287 1.04 13.806 10.469 17.358 21.32zm-27.638-19.483c2.304 5.773 3.802 14.058 3.802 25.238 0 .572-.005 1.095-.01 1.624-9.117 2.824-19.024 5.89-28.953 8.966 5.575-21.516 16.025-31.908 25.161-35.828zm-11.131-10.537c1.617 0 3.246.549 4.805 1.622-12.007 5.65-24.877 19.88-30.312 48.297l-22.886 7.088C75.694 46.16 90.81 10.828 117.72 10.828z"
fill="#95BF46"
/>
<path
d="M221.237 54.983c-1.055-.088-23.383-1.743-23.383-1.743s-15.507-15.395-17.209-17.099c-.637-.634-1.496-.959-2.394-1.099l-12.527 256.233 89.762-19.418S223.972 58.8 223.774 57.34c-.201-1.46-1.48-2.268-2.537-2.357"
fill="#5E8E3E"
/>
<path
d="M135.242 104.585l-11.069 32.926s-9.698-5.176-21.586-5.176c-17.428 0-18.305 10.937-18.305 13.693 0 15.038 39.2 20.8 39.2 56.024 0 27.713-17.577 45.558-41.277 45.558-28.44 0-42.984-17.7-42.984-17.7l7.615-25.16s14.95 12.835 27.565 12.835c8.243 0 11.596-6.49 11.596-11.232 0-19.616-32.16-20.491-32.16-52.724 0-27.129 19.472-53.382 58.778-53.382 15.145 0 22.627 4.338 22.627 4.338"
fill="#FFF"
/>
</svg>
);
export const TwitchIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2400 2800"
xmlSpace="preserve"
fill="currentColor"
{...props}
>
<path fill="#fff" d="m2200 1300-400 400h-400l-350 350v-350H600V200h1600z" />
<g fill="#9146ff">
<path d="M500 0 0 500v1800h600v500l500-500h400l900-900V0H500zm1700 1300-400 400h-400l-350 350v-350H600V200h1600v1100z" />
<path d="M1700 550h200v600h-200zm-550 0h200v600h-200z" />
</g>
</svg>
);
export const YoutubeIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 256 180"
width={256}
height={180}
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
fill="currentColor"
{...props}
>
<path
d="M250.346 28.075A32.18 32.18 0 0 0 227.69 5.418C207.824 0 127.87 0 127.87 0S47.912.164 28.046 5.582A32.18 32.18 0 0 0 5.39 28.24c-6.009 35.298-8.34 89.084.165 122.97a32.18 32.18 0 0 0 22.656 22.657c19.866 5.418 99.822 5.418 99.822 5.418s79.955 0 99.82-5.418a32.18 32.18 0 0 0 22.657-22.657c6.338-35.348 8.291-89.1-.164-123.134Z"
fill="red"
/>
<path fill="#FFF" d="m102.421 128.06 66.328-38.418-66.328-38.418z" />
</svg>
);
4
Update the import paths to match your project setup
Props
Prop | Type | Default | Description |
---|---|---|---|
name | string | Alex Morgan | The name to display in the animated form component. |
delay | number | 7000 | Time interval (in milliseconds) after which the animation restarts. |