feat: animate page transitions with Motion

This commit is contained in:
2026-09-09 19:00:24 +03:00
parent 91eaeb5661
commit c906dfa2e2
4 changed files with 93 additions and 1 deletions
+61
View File
@@ -14,6 +14,7 @@
"@tanstack/react-query": "5.101.4", "@tanstack/react-query": "5.101.4",
"dayjs": "1.11.23", "dayjs": "1.11.23",
"i18next": "26.0.2", "i18next": "26.0.2",
"motion": "13.2.0",
"openapi-fetch": "0.17.0", "openapi-fetch": "0.17.0",
"openapi-react-query": "0.5.4", "openapi-react-query": "0.5.4",
"react": "19.2.8", "react": "19.2.8",
@@ -4921,6 +4922,29 @@
"node": ">=18.3.0" "node": ">=18.3.0"
} }
}, },
"node_modules/framer-motion": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.2.0.tgz",
"integrity": "sha512-9E33ebgMaO33w1nN/jEdW8z3/GO483fMi4rqbMG9rt83XgW9QLKRe4NcmJ8s+fQ3O34++UHrIQwlIWGIWTITjA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^13.2.0",
"motion-utils": "^13.0.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -6277,6 +6301,43 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/motion": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/motion/-/motion-13.2.0.tgz",
"integrity": "sha512-4Hrb5vD6HhjFstLUiCmWvtpsw+WTpP4R+QXfSDYZBz7+uxE/LrRg3aV0ReJxHrTRffHhbIE6svEqnotngXvesQ==",
"license": "MIT",
"dependencies": {
"framer-motion": "^13.2.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.2.0.tgz",
"integrity": "sha512-N6gdSoWRDk0Rh/fVtlqUtLs+fEN3ELFZI3cn3IQE9Mnf3E+Mh8wjO6MstzCOPFh4Yf0L1as5m2eUyYWj8ylVSQ==",
"license": "MIT",
"dependencies": {
"motion-utils": "^13.0.0"
}
},
"node_modules/motion-utils": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz",
"integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+1
View File
@@ -36,6 +36,7 @@
"@tanstack/react-query": "5.101.4", "@tanstack/react-query": "5.101.4",
"dayjs": "1.11.23", "dayjs": "1.11.23",
"i18next": "26.0.2", "i18next": "26.0.2",
"motion": "13.2.0",
"openapi-fetch": "0.17.0", "openapi-fetch": "0.17.0",
"openapi-react-query": "0.5.4", "openapi-react-query": "0.5.4",
"react": "19.2.8", "react": "19.2.8",
+6 -1
View File
@@ -2,6 +2,7 @@ import { Container, Group, Skeleton, Stack } from "@mantine/core";
import type { FC, PropsWithChildren } from "react"; import type { FC, PropsWithChildren } from "react";
import { Suspense } from "react"; import { Suspense } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { RouteTransition } from "./RouteTransition";
type RouteSkeletonVariant = "auth" | "content" | "shell"; type RouteSkeletonVariant = "auth" | "content" | "shell";
type LazyPageProps = PropsWithChildren<{ variant: RouteSkeletonVariant }>; type LazyPageProps = PropsWithChildren<{ variant: RouteSkeletonVariant }>;
@@ -60,7 +61,11 @@ const LazyPage: FC<LazyPageProps> = (props) => {
const { children, variant } = props; const { children, variant } = props;
return ( return (
<Suspense fallback={<RouteSkeleton variant={variant} />}> <Suspense fallback={<RouteSkeleton variant={variant} />}>
{children} {variant === "shell" ? (
children
) : (
<RouteTransition>{children}</RouteTransition>
)}
</Suspense> </Suspense>
); );
}; };
+25
View File
@@ -0,0 +1,25 @@
import { domAnimation, LazyMotion, useReducedMotion } from "motion/react";
import * as m from "motion/react-m";
import type { FC, PropsWithChildren } from "react";
import { useLocation } from "react-router";
const RouteTransition: FC<PropsWithChildren> = (props) => {
const { children } = props;
const { pathname } = useLocation();
const reduceMotion = useReducedMotion();
return (
<LazyMotion features={domAnimation}>
<m.div
key={pathname}
initial={reduceMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: reduceMotion ? 0 : 0.2, ease: "easeOut" }}
>
{children}
</m.div>
</LazyMotion>
);
};
export { RouteTransition };