Animate page transition

I am trying to animate route changes with Motion One. However, I seem to not get the overall logic of Solid Router since no route is shown at all in my MWE:
import {render} from 'solid-js/web';
import {A, Route, Router} from '@solidjs/router';
import {Motion} from 'solid-motionone';


const Home = () => {
return (
<>
<p>Home</p>
<A href="/about">About</A>
</>
)
}

const About = () => {
return (
<>
<p>About</p>
<A href="/">Home</A>
</>
)
}

const RouteAnimation = (props: any) => (
<Motion
animate={{opacity: 0.75, transition: {duration: 1}}}
exit={{opacity: 0.25, transition: {duration: 1}}}
>
<Route path={props.path} component={props.component}/>
</Motion>
);

const App = () => {
return (
<>
<Router>
<RouteAnimation path="/" component={Home}/>
<RouteAnimation path="/about" component={About}/>
</Router>

</>
)
}

render(() => <App/>, document.getElementById('root')!);
import {render} from 'solid-js/web';
import {A, Route, Router} from '@solidjs/router';
import {Motion} from 'solid-motionone';


const Home = () => {
return (
<>
<p>Home</p>
<A href="/about">About</A>
</>
)
}

const About = () => {
return (
<>
<p>About</p>
<A href="/">Home</A>
</>
)
}

const RouteAnimation = (props: any) => (
<Motion
animate={{opacity: 0.75, transition: {duration: 1}}}
exit={{opacity: 0.25, transition: {duration: 1}}}
>
<Route path={props.path} component={props.component}/>
</Motion>
);

const App = () => {
return (
<>
<Router>
<RouteAnimation path="/" component={Home}/>
<RouteAnimation path="/about" component={About}/>
</Router>

</>
)
}

render(() => <App/>, document.getElementById('root')!);
Any help would be a appreciated.
8 Replies
Madaxen86
Madaxen863mo ago
Maybe instead of wrapping the route in Motion component you may create a layout which wraps the children (in this case the whole pages) in the Motion component. The Router component expects Route components as children. https://docs.solidjs.com/solid-router/concepts/layouts#layouts
sonovice
sonoviceOP3mo ago
Thanks for the hint. I am still having a hard time to get it right. Only the initial page load is animated, other transitions are not. Current code:
import {render} from "solid-js/web";
import {A, HashRouter, Route} from "@solidjs/router";
import {Motion, Presence} from "solid-motionone";
import "./styles.css";

const Home = () => (
<div class="blue">
<p>Home</p>
<A href="/about">About</A>
</div>
);

const About = () => (
<div class="orange">
<p>About</p>
<A href="/">Home</A>
</div>
);

const Layout = (props: any) => {
return (
<div>
<Presence exitBeforeEnter>
<Motion
initial={{transform: "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: "translateX(-100%)"}}
transition={{duration: 0.3}}
>
{props.children}
</Motion>
</Presence>
</div>
);
};


const App = () => (
<HashRouter root={Layout}>
<Route path="/" component={Home}/>
<Route path="/about" component={About}/>
</HashRouter>
);

// Render the App
render(() => <App/>, document.getElementById("root")!);
import {render} from "solid-js/web";
import {A, HashRouter, Route} from "@solidjs/router";
import {Motion, Presence} from "solid-motionone";
import "./styles.css";

const Home = () => (
<div class="blue">
<p>Home</p>
<A href="/about">About</A>
</div>
);

const About = () => (
<div class="orange">
<p>About</p>
<A href="/">Home</A>
</div>
);

const Layout = (props: any) => {
return (
<div>
<Presence exitBeforeEnter>
<Motion
initial={{transform: "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: "translateX(-100%)"}}
transition={{duration: 0.3}}
>
{props.children}
</Motion>
</Presence>
</div>
);
};


const App = () => (
<HashRouter root={Layout}>
<Route path="/" component={Home}/>
<Route path="/about" component={About}/>
</HashRouter>
);

// Render the App
render(() => <App/>, document.getElementById("root")!);
Madaxen86
Madaxen863mo ago
Oh yeah sure, Layouts persist on route changes. This should do the trick for a root layout:
const Layout = (props: any) => {
const trigger = useIsRouting();
//prevent children from rendering twice
const c = children(() => props.children);

return (
<div>
<Presence exitBeforeEnter>
<Show when={!trigger()}>
<Motion.div
initial={{ transform: 'translateX(100%)' }}
animate={{ transform: 'translateX(0%)' }}
exit={{ transform: 'translateX(-100%)' }}
transition={{ duration: 0.3 }}
>
{c()}
</Motion.div>
</Show>
</Presence>
</div>
);
};
const Layout = (props: any) => {
const trigger = useIsRouting();
//prevent children from rendering twice
const c = children(() => props.children);

return (
<div>
<Presence exitBeforeEnter>
<Show when={!trigger()}>
<Motion.div
initial={{ transform: 'translateX(100%)' }}
animate={{ transform: 'translateX(0%)' }}
exit={{ transform: 'translateX(-100%)' }}
transition={{ duration: 0.3 }}
>
{c()}
</Motion.div>
</Show>
</Presence>
</div>
);
};
sonovice
sonoviceOP3mo ago
Thanks! This kinda works, but now I have a blank screen between two pages. The animation for the next page only starts after the exit animation of the previous one finished. If I remove exitBeforeEnter, there is no exit animation. It seems as if it impossible to trigger exit and animate at the same time with solid router :/
Madaxen86
Madaxen863mo ago
Yes, that's the nature of routing. You can only show one route at a time. If I get your use case correctly you want to show two components exiting and entering at the same time: Then I would use somthing like a carousel component (like https://shadcn-solid.com/docs/components/carousel) and instead of using the hashrouter you could use the useSearchParams hook to control the carousel
sonovice
sonoviceOP3mo ago
Ah, got it. So I have to have all pages of my app already in the DOM, otherwise things vanish. Thank you! I took your ideas and came up with this, kinda abusing the router:
import {render} from "solid-js/web";
import {A, HashRouter, useLocation} from "@solidjs/router";
import {Motion, Presence} from "solid-motionone";
import {Match, Switch} from "solid-js";
import "./styles.css";

const Home = () => (
<div class="blue">
<p>Home</p>
<A href="/about">About</A>
</div>
);

const About = () => (
<div class="orange">
<p>About</p>
<A href="/">Home</A>
</div>
);

const SlideInAnimation = (props: any) => (
<Motion
initial={{transform: "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: "translateX(-100%)"}}
transition={{duration: 0.3}}
style="position: absolute; width: 100%; height: 100%;"
>
{props.component}
</Motion>
)


const Layout = () => {
const currentPath = () => useLocation().pathname;

return (
<div>
<Presence initial={false}>
<Switch>

{/* Home Route */}
<Match when={currentPath() === "/"}>
<SlideInAnimation component={<Home/>}/>
</Match>

{/* About Route */}
<Match when={currentPath() === "/about"}>
<SlideInAnimation component={<About/>}/>
</Match>

</Switch>
</Presence>
</div>
);
};


const App = () => (
<HashRouter root={Layout}/>
);

// Render the App
render(() => <App/>, document.getElementById("root")!);
import {render} from "solid-js/web";
import {A, HashRouter, useLocation} from "@solidjs/router";
import {Motion, Presence} from "solid-motionone";
import {Match, Switch} from "solid-js";
import "./styles.css";

const Home = () => (
<div class="blue">
<p>Home</p>
<A href="/about">About</A>
</div>
);

const About = () => (
<div class="orange">
<p>About</p>
<A href="/">Home</A>
</div>
);

const SlideInAnimation = (props: any) => (
<Motion
initial={{transform: "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: "translateX(-100%)"}}
transition={{duration: 0.3}}
style="position: absolute; width: 100%; height: 100%;"
>
{props.component}
</Motion>
)


const Layout = () => {
const currentPath = () => useLocation().pathname;

return (
<div>
<Presence initial={false}>
<Switch>

{/* Home Route */}
<Match when={currentPath() === "/"}>
<SlideInAnimation component={<Home/>}/>
</Match>

{/* About Route */}
<Match when={currentPath() === "/about"}>
<SlideInAnimation component={<About/>}/>
</Match>

</Switch>
</Presence>
</div>
);
};


const App = () => (
<HashRouter root={Layout}/>
);

// Render the App
render(() => <App/>, document.getElementById("root")!);
Madaxen86
Madaxen863mo ago
Legit solution 👍
sonovice
sonoviceOP3mo ago
Now I have to figure out how to change the transformations dynamically based on previous and next route. Basically "slide right" if I go deeper in the path structure (e.g. "/" -> "/dashboard/") and "slide left" if I go up. Done. FYI:
[...]

const SlideAnimation: Component<{
component: Component,
direction: "left" | "right"
}> = (props) => (
<Motion
initial={{transform: props.direction === "left" ? "translateX(-100%)" : "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: props.direction === "left" ? "translateX(100%)" : "translateX(-100%)"}}
transition={{duration: 0.3}}
style="position: absolute; width: 100%; height: 100%;"
>
<props.component/>
</Motion>
)

const Layout: Component = () => {
const [previousPath, setPreviousPath] = createSignal<string>("/");
const [slideDirection, setSlideDirection] = createSignal<"left" | "right">("right");
const currentPath = () => useLocation().pathname;

function calcDirection() {
const normalizedCurrentPath = currentPath().replace(/\/$/, "");
const normalizedPreviousPath = previousPath().replace(/\/$/, "");
const depth = normalizedCurrentPath.split("/").length - normalizedPreviousPath.split("/").length;
setSlideDirection(depth < 0 ? "right" : "left");
}

createEffect(() => {
if (currentPath() !== previousPath()) {
calcDirection();
setPreviousPath(currentPath());
}
})

return (
<div>
<Presence initial={false}>
<Switch>

<Match when={currentPath() === "/"}>
<SlideAnimation component={Home} direction={slideDirection()}/>
</Match>

<Match when={currentPath() === "/about"}>
<SlideAnimation component={About} direction={slideDirection()}/>
</Match>

</Switch>
</Presence>
</div>
);
}


const App = () => (
<HashRouter root={Layout}/>
);

render(() => <App/>, document.getElementById("root")!);
[...]

const SlideAnimation: Component<{
component: Component,
direction: "left" | "right"
}> = (props) => (
<Motion
initial={{transform: props.direction === "left" ? "translateX(-100%)" : "translateX(100%)"}}
animate={{transform: "translateX(0%)"}}
exit={{transform: props.direction === "left" ? "translateX(100%)" : "translateX(-100%)"}}
transition={{duration: 0.3}}
style="position: absolute; width: 100%; height: 100%;"
>
<props.component/>
</Motion>
)

const Layout: Component = () => {
const [previousPath, setPreviousPath] = createSignal<string>("/");
const [slideDirection, setSlideDirection] = createSignal<"left" | "right">("right");
const currentPath = () => useLocation().pathname;

function calcDirection() {
const normalizedCurrentPath = currentPath().replace(/\/$/, "");
const normalizedPreviousPath = previousPath().replace(/\/$/, "");
const depth = normalizedCurrentPath.split("/").length - normalizedPreviousPath.split("/").length;
setSlideDirection(depth < 0 ? "right" : "left");
}

createEffect(() => {
if (currentPath() !== previousPath()) {
calcDirection();
setPreviousPath(currentPath());
}
})

return (
<div>
<Presence initial={false}>
<Switch>

<Match when={currentPath() === "/"}>
<SlideAnimation component={Home} direction={slideDirection()}/>
</Match>

<Match when={currentPath() === "/about"}>
<SlideAnimation component={About} direction={slideDirection()}/>
</Match>

</Switch>
</Presence>
</div>
);
}


const App = () => (
<HashRouter root={Layout}/>
);

render(() => <App/>, document.getElementById("root")!);
Want results from more Discord servers?
Add your server