Skip to content

Commit 195596d

Browse files
committed
feat: improve naivgation animations
1 parent a6967f6 commit 195596d

2 files changed

Lines changed: 78 additions & 26 deletions

File tree

src/components/navigation/NavigationDesktop.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ type NavigationDesktopProps = {
3030
}
3131

3232
const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, buttons }) => {
33+
const rootRef = useRef<HTMLDivElement>(null)
3334
const navTabsRef = useRef<HTMLDivElement>(null)
3435
const submenuContentRef = useRef<HTMLDivElement>(null)
3536
const cursorRef = useRef<NavigationCursorHandle>(null)
3637
const suppressMenuOpenRef = useRef(false)
3738
const [activeMenuValue, setActiveMenuValue] = useState<string | null>(null)
39+
const [shellInsetWidth, setShellInsetWidth] = useState(0)
3840
const [submenuHeight, setSubmenuHeight] = useState(0)
3941
const { homeHref, navbarItems, navbarButtons } = useNavigationViewModel(locale, items, buttons)
4042
const isScrolled = useNavigationScrollState({
@@ -49,11 +51,13 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
4951
const navTriggerClassName = cn(navItemClassName, "h-auto pr-1 bg-transparent text-base text-white hover:bg-transparent focus:bg-transparent data-popup-open:bg-transparent data-open:bg-transparent")
5052
const activeMenuItem = navbarItems.find((item) => item.title === activeMenuValue)
5153
const activeSubMenu = activeMenuItem?.subMenu?.length ? activeMenuItem.subMenu : null
54+
const scrolledRightInsetOffset = 6
5255
const shellTransition = {
53-
type: "spring",
54-
stiffness: 40,
55-
damping: 10,
56+
duration: 0.5,
57+
ease: [0.16, 1, 0.3, 1],
5658
} as const
59+
const shellInset = isScrolled ? shellInsetWidth : 0
60+
const shellRightInset = isScrolled ? shellInsetWidth + scrolledRightInsetOffset : 0
5761

5862
useEffect(() => {
5963
if (!isScrolled) return
@@ -82,6 +86,26 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
8286
return () => resizeObserver.disconnect()
8387
}, [activeSubMenu, isScrolled])
8488

89+
useLayoutEffect(() => {
90+
const element = rootRef.current
91+
92+
if (!element) {
93+
setShellInsetWidth(0)
94+
return
95+
}
96+
97+
const measure = () => {
98+
setShellInsetWidth(element.clientWidth * 0.1)
99+
}
100+
101+
measure()
102+
103+
const resizeObserver = new ResizeObserver(measure)
104+
resizeObserver.observe(element)
105+
106+
return () => resizeObserver.disconnect()
107+
}, [])
108+
85109
const updateIndicatorFromElement = (element: HTMLElement) => {
86110
if (!navTabsRef.current) return
87111
cursorRef.current?.moveTo(element, navTabsRef.current)
@@ -91,6 +115,7 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
91115
<div className="fixed z-50 h-max w-full pt-3">
92116
<Container>
93117
<div
118+
ref={rootRef}
94119
className="relative my-4"
95120
onPointerMove={() => {
96121
suppressMenuOpenRef.current = false
@@ -105,22 +130,19 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
105130
"pointer-events-none absolute inset-0 rounded-2xl shadow-sm",
106131
isScrolled ? "bg-primary/50 backdrop-blur-lg" : "bg-transparent shadow-none",
107132
)}
108-
initial={{
109-
clipPath: "inset(0% 0% 0% 0% round 1rem)",
110-
}}
133+
initial={false}
111134
animate={{
112-
clipPath: isScrolled
113-
? "inset(0% 10% 0% 10% round 1rem)"
114-
: "inset(0% 0% 0% 0% round 1rem)",
135+
left: shellInset,
136+
right: shellInset,
115137
}}
116138
transition={shellTransition}
117139
/>
118140
<motion.div
119141
className="pointer-events-none absolute inset-0 z-10 rounded-2xl border border-white/5"
120142
initial={false}
121143
animate={{
122-
left: isScrolled ? "10%" : "0%",
123-
right: isScrolled ? "10%" : "0%",
144+
left: shellInset,
145+
right: shellInset,
124146
opacity: isScrolled ? 1 : 0,
125147
}}
126148
transition={shellTransition}
@@ -129,8 +151,8 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
129151
className="relative z-10 flex flex-col overflow-visible rounded-2xl p-1.5"
130152
initial={false}
131153
animate={{
132-
paddingLeft: isScrolled ? "10%" : "0%",
133-
paddingRight: isScrolled ? "calc(10% + 0.5rem)" : "0%",
154+
paddingLeft: shellInset,
155+
paddingRight: shellRightInset,
134156
}}
135157
transition={shellTransition}
136158
>

src/components/navigation/NavigationMobile.tsx

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IconMenu2, IconX } from "@tabler/icons-react"
1212
import { AnimatePresence, m as motion } from "motion/react"
1313
import Image from "next/image"
1414
import Link from "next/link"
15-
import React, { useEffect, useRef, useState } from "react"
15+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
1616
import { useWebHaptics } from "web-haptics/react"
1717
import { NavigationLink } from "./NavigationLink"
1818
import { NavigationMobileItem } from "./NavigationMobileItem"
@@ -26,10 +26,12 @@ type NavigationMobileProps = {
2626
const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, buttons }) => {
2727
const { trigger } = useWebHaptics()
2828
const menuRef = useOutsideClick<HTMLElement>(() => setIsOpen(false))
29+
const rootRef = useRef<HTMLDivElement>(null)
2930
const wasOpenRef = useRef(false)
3031
const [isOpen, setIsOpen] = useState(false)
3132
const [mobileOpenKey, setMobileOpenKey] = useState<string | null>(null)
3233
const [isMenuClosing, setIsMenuClosing] = useState(false)
34+
const [shellInsetWidth, setShellInsetWidth] = useState(0)
3335
const { homeHref, navbarItems, navbarButtons } = useNavigationViewModel(locale, items, buttons)
3436
const menuTransition = {
3537
duration: 0.34,
@@ -43,6 +45,18 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
4345
const isScrolled = useNavigationScrollState({
4446
onScroll: () => setIsOpen((prevIsOpen) => (prevIsOpen ? false : prevIsOpen)),
4547
})
48+
const expandedHeaderPadding = 8
49+
const shellInset = isScrolled && !isShellExpanded ? shellInsetWidth : 0
50+
const headerInlinePadding = isShellExpanded
51+
? expandedHeaderPadding
52+
: isScrolled
53+
? shellInsetWidth
54+
: 0
55+
const headerRightPadding = isShellExpanded
56+
? expandedHeaderPadding
57+
: isScrolled
58+
? shellInsetWidth + expandedHeaderPadding
59+
: 0
4660

4761
useEffect(() => {
4862
let timeoutId: ReturnType<typeof setTimeout> | undefined
@@ -63,6 +77,26 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
6377
}
6478
}, [isOpen])
6579

80+
useLayoutEffect(() => {
81+
const element = rootRef.current
82+
83+
if (!element) {
84+
setShellInsetWidth(0)
85+
return
86+
}
87+
88+
const measure = () => {
89+
setShellInsetWidth(element.clientWidth * 0.1)
90+
}
91+
92+
measure()
93+
94+
const resizeObserver = new ResizeObserver(measure)
95+
resizeObserver.observe(element)
96+
97+
return () => resizeObserver.disconnect()
98+
}, [])
99+
66100
const closeMenu = () => {
67101
trigger("medium")
68102
setIsOpen(false)
@@ -75,26 +109,25 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
75109
ref={menuRef}
76110
>
77111
<Container>
78-
<div className="relative my-6">
112+
<div ref={rootRef} className="relative my-6">
79113
<motion.div
80114
className={cn(
81115
"pointer-events-none absolute inset-0 rounded-2xl shadow-sm",
82116
(isScrolled || isShellExpanded) ? "bg-primary/50 backdrop-blur-lg" : "bg-transparent shadow-none",
83117
)}
84118
initial={false}
85119
animate={{
86-
clipPath: isScrolled && !isShellExpanded
87-
? "inset(0% 10% 0% 10% round 1rem)"
88-
: "inset(0% 0% 0% 0% round 1rem)",
120+
left: shellInset,
121+
right: shellInset,
89122
}}
90123
transition={shellTransition}
91124
/>
92125
<motion.div
93126
className="pointer-events-none absolute inset-0 z-10 rounded-2xl border border-white/5"
94127
initial={false}
95128
animate={{
96-
left: isScrolled && !isShellExpanded ? "10%" : "0%",
97-
right: isScrolled && !isShellExpanded ? "10%" : "0%",
129+
left: shellInset,
130+
right: shellInset,
98131
opacity: isScrolled || isShellExpanded ? 1 : 0,
99132
}}
100133
transition={shellTransition}
@@ -107,8 +140,8 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
107140
className={"w-full flex items-center justify-between gap-2"}
108141
initial={false}
109142
animate={{
110-
paddingLeft: isScrolled ? "calc(10% + 0rem)" : "calc(0% + 0rem)",
111-
paddingRight: isScrolled ? "calc(10% + 0.5rem)" : "calc(0% + 0rem)",
143+
paddingLeft: headerInlinePadding,
144+
paddingRight: headerRightPadding,
112145
}}
113146
transition={shellTransition}
114147
>
@@ -146,10 +179,7 @@ const NavigationMobile: React.FC<NavigationMobileProps> = ({ locale, items, butt
146179
initial={{ opacity: 0, y: -4, height: 0 }}
147180
animate={{ opacity: 1, y: 0, height: "auto" }}
148181
exit={{ opacity: 0, y: -4, height: 0 }}
149-
transition={{
150-
...menuTransition,
151-
delay: isScrolled ? 0.08 : 0,
152-
}}
182+
transition={menuTransition}
153183
style={{ overflow: "hidden" }}
154184
className="flex flex-col gap-2 px-2"
155185
>

0 commit comments

Comments
 (0)