profile picture of Olivier Larose

Olivier Larose

October 30, 2023

/

Beginner

/

Medium

Awwwards Side Menu

Rebuild an Awwwards Side Menu with Framer motion and Nextjs

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 Tutorial
background video

Initializing the project

Let'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.

  • We will use Sass for the stylesheets, so we can run npm i sass.
  • We will use Framer Motion for the animation, so we can run npm i framer-motion.

Header Component

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

}

Creating the Button

Then the first element I'll work on is the button.

  • At the root of the layout is a button that has the responsibility of toggling the menu. All of that is managed with the 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.

  • The top property is animated with Framer Motion when the menu is active and with an overflow hidden it creates this effect.
  • The perspective effect is created with CSS on hover where the 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.div

8

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

<div

13

className={styles.el}

14

onClick={() => {toggleMenu()}}

15

>

16

<PerspectiveText label="Menu"/>

17

</div>

18

<div

19

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

}

We should have something like this:

Creating the Menu's window

The window is animated with Framer Motion, a couple of properties are modified:

  • Top and Left: are slightly changed to create a gap between the button and the window.
  • Height and Width: to animate the opening and closing of window.

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.div

26

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

}

We should have something like this:

Adding the Nav

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:

  • Opacity: from 0 to 1
  • RotateX: from 90 to 0
  • TranslateY: from 80 to 0
  • TranslateX: from -20 to 0

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.div

16

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

}

We should have something like this:

Adding the Footer

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.a

16

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

}

We should have something like this:

Wrapping up

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

Related Animations

image

Jul 13, 2023

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

image

Jul 4, 2023

Curved Menu

A website tutorial on how to make an awwwards curved menu using Nextjs, GSAP and Framer Motion. A curve is created using SVG path commands Inspired by https://dennissnellenberg.com/.

image

June 28, 2023

Navigation Menu

A website tutorial on how to make an Awwwards Navigation bar menu using Nextjs and Framer Motion. Inspired by the awwwards winning website https://props.studiolumio.com/