Sliding Stairs Menu
A website tutorial featuring an animated menu that comes from an awwwards winning website. Features a stair-like animation with an infinite slider. Inspired by https://k72.ca/travail
Olivier Larose
October 30, 2023
/
Beginner
/
Medium
A website tutorial featuring the rebuild of a menu from an Awwwards winning website, made with Framer Motion and Next.js, inspired by: https://agencecartier.com/fr
Live DemoSource codeVideo TutorialLet's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client
inside of a terminal.
We can delete everything in the page.js
, global.css
and page.module.css
and add our own HTML and CSS, to start with a nice blank application.
npm i sass
.npm i framer-motion
.The Header component will be placed at the root layout so it's shared among all pages.
app/layout.jsx
1
import { Inter } from 'next/font/google'2
import './globals.css'3
import Header from '../components/Header';4
5
const inter = Inter({ subsets: ['latin'] })6
7
export const metadata = {8
title: 'Create Next App',9
description: 'Generated by create next app',10
}11
12
export default function RootLayout({ children }) {13
return (14
<html lang="en">15
<body className={inter.className}>16
<Header />17
{children}18
</body>19
</html>20
)21
}
Then the first element I'll work on is the button.
useState
hook.app/layo...
Header/s...
1
'use client';2
import { useState } from 'react'3
import Button from './Button';4
import styles from './style.module.scss';5
6
export default function index() {7
const [isActive, setIsActive] = useState(false);8
return (9
<div className={styles.header}>10
<Button isActive={isActive} toggleMenu={() => {setIsActive(!isActive)}}/>11
</div>12
)13
}
The button is mostly made with HTML and CSS, but there's a small Framer Motion animation.
top
property is animated with Framer Motion when the menu is active and with an overflow hidden it creates this effect.rotateX
value is animated.Button/i...
Button/s...
1
import { motion } from 'framer-motion';2
import styles from './style.module.scss';3
4
export default function Button({isActive, toggleMenu}) {5
return (6
<div className={styles.button}>7
<motion.div8
className={styles.slider}9
animate={{top: isActive ? "-100%" : "0%"}}10
transition={{ duration: 0.5, type: "tween", ease: [0.76, 0, 0.24, 1]}}11
>12
<div13
className={styles.el}14
onClick={() => {toggleMenu()}}15
>16
<PerspectiveText label="Menu"/>17
</div>18
<div19
className={styles.el}20
onClick={() => {toggleMenu()}}21
>22
<PerspectiveText label="Close" />23
</div>24
</motion.div>25
</div>26
)27
}28
29
function PerspectiveText({label}) {30
return (31
<div className={styles.perspectiveText}>32
<p>{label}</p>33
<p>{label}</p>34
</div>35
)36
}
The window is animated with Framer Motion, a couple of properties are modified:
Header/i...
Header/s...
1
...2
import { motion } from 'framer-motion';3
4
const menu = {5
open: {6
width: "480px",7
height: "650px",8
top: "-25px",9
right: "-25px",10
transition: { duration: 0.75, type: "tween", ease: [0.76, 0, 0.24, 1]}11
},12
closed: {13
width: "100px",14
height: "40px",15
top: "0px",16
right: "0px",17
transition: { duration: 0.75, delay: 0.35, type: "tween", ease: [0.76, 0, 0.24, 1]}18
}19
}20
21
export default function index() {22
const [isActive, setIsActive] = useState(false);23
return (24
<div className={styles.header}>25
<motion.div26
className={styles.menu}27
variants={menu}28
animate={isActive ? "open" : "closed"}29
initial="closed"30
>31
</motion.div>32
<Button isActive={isActive} toggleMenu={() => {setIsActive(!isActive)}}/>33
</div>34
)35
}
The Nav is made by mapping an array of objects (each represent a link).
Nav/data.js
1
export const links = [2
{3
title: "Projects",4
href: "/"5
},6
{7
title: "Agency",8
href: "/"9
},10
{11
title: "Expertise",12
href: "/"13
},14
...15
]
Then for each link object, a div
and a a
is returned. The div is then animated using these properties:
The parent of each link have a perspective: 120px
and perspective-origin: bottom
, which enhances the effect of the above properties.
Note: The delay between each animation is made through the custom
property of Framer Motion, which is given to the definiton of the variants.
Nav/inde...
Nav/styl...
Nav/anim...
1
import styles from './style.module.scss';2
import { motion } from 'framer-motion';3
import { links } from './data';4
import { perspective } from "./anim";5
6
export default function index() {7
return (8
<div className={styles.nav}>9
<div className={styles.body}>10
{11
links.map( (link, i) => {12
const { title, href } = link;13
return (14
<div key={`b_${i}`} className={styles.linkContainer}>15
<motion.div16
href={href}17
custom={i}18
variants={perspective}19
initial="initial"20
animate="enter"21
exit="exit"22
>23
<a>24
{title}25
</a>26
</motion.div>27
</div>28
)29
})30
}31
</div>32
</div>33
)34
}
The implementation of the footer is very similar to the implementation of the Nav. However, instead of animating a bunch of different properties, only the y value is modified.
Nav/data.js
1
...2
3
export const footerLinks = [4
{5
title: "Facebook",6
href: "/"7
},8
{9
title: "LinkedIn",10
href: "/"11
},12
{13
title: "Instagram",14
href: "/"15
},16
{17
title: "Twitter",18
href: "/"19
}20
]
Nav/inde...
Nav/styl...
Nav/anim...
1
import styles from './style.module.scss';2
import { motion } from 'framer-motion';3
import { links, footerLinks } from './data';4
import { perspective, slideIn } from "./anim";5
6
export default function index() {7
return (8
<div className={styles.nav}>9
...10
<motion.div className={styles.footer}>11
{12
footerLinks.map( (link, i) => {13
const { title, href } = link;14
return (15
<motion.a16
variants={slideIn}17
custom={i}18
initial="initial"19
animate="enter"20
exit="exit"21
key={`f_${i}`}22
>23
{title}24
</motion.a>25
)26
})27
}28
</motion.div>29
</div>30
)31
}
That's it for this animation!
Very nice menu by the super sick agency Agence Cartier, nice to see how slick of an effect we can get by using CSS perspectives with rotation values! Hope you learned something.
-Oli