Skip to content

Commit 4f9c065

Browse files
lwinmoepaingclaude
andcommitted
⚡ perf(navbar): fix mobile avatar dropdown with floating-ui, reduce animations
Fix UserAvatar dropdown positioning on mobile by migrating to @floating-ui/react with FloatingPortal. Separate positioning (outer div) from animation (inner motion.div) to prevent motion overriding floating-ui transforms. Remove GPU-heavy blur filter animations from mobile nav links, replace animated motion.div wrappers with static divs, and add xs:375px Tailwind breakpoint for iPhone 6 font scaling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f907415 commit 4f9c065

3 files changed

Lines changed: 68 additions & 67 deletions

File tree

src/components/Auth/UserAvatar.tsx

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useRef, useEffect } from "react";
3+
import { useState } from "react";
44
import { useAuth } from "@/hooks/useAuth";
55
import { cn } from "@/utils";
66
import { motion, AnimatePresence } from "motion/react";
@@ -9,34 +9,51 @@ import MseLink from "@/components/Ui/MseLink/MseLink";
99
import { useTranslations } from "next-intl";
1010
import { useLanguage } from "@/hooks/useLanguage";
1111
import { khitHaungg } from "@/fonts/fonts";
12+
import {
13+
useFloating,
14+
offset,
15+
flip,
16+
shift,
17+
autoUpdate,
18+
useClick,
19+
useDismiss,
20+
useInteractions,
21+
FloatingPortal,
22+
} from "@floating-ui/react";
1223

1324
export default function UserAvatar() {
1425
const { user, signOut, isAdmin } = useAuth();
1526
const [open, setOpen] = useState(false);
16-
const ref = useRef<HTMLDivElement>(null);
1727
const t = useTranslations("auth");
1828
const tBlog = useTranslations("blog");
1929
const { isMyanmar } = useLanguage();
2030
const mmFont = isMyanmar ? khitHaungg.className : "";
2131

22-
useEffect(() => {
23-
const onClickOutside = (e: MouseEvent) => {
24-
if (ref.current && !ref.current.contains(e.target as Node)) {
25-
setOpen(false);
26-
}
27-
};
28-
document.addEventListener("mousedown", onClickOutside);
29-
return () => document.removeEventListener("mousedown", onClickOutside);
30-
}, []);
32+
const { refs, floatingStyles, context } = useFloating({
33+
open,
34+
onOpenChange: setOpen,
35+
placement: "bottom-end",
36+
middleware: [
37+
offset(12),
38+
flip({ fallbackPlacements: ["top-end", "top-start", "bottom-start"] }),
39+
shift({ padding: 8 }),
40+
],
41+
whileElementsMounted: autoUpdate,
42+
});
43+
44+
const click = useClick(context);
45+
const dismiss = useDismiss(context);
46+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
3147

3248
if (!user) return null;
3349

3450
return (
35-
<div ref={ref} className="relative">
51+
<div className="relative">
3652
{/* Avatar button with prismatic ring */}
3753
<motion.button
54+
ref={refs.setReference}
55+
{...getReferenceProps()}
3856
type="button"
39-
onClick={() => setOpen(!open)}
4057
className="relative w-9 h-9 rounded-full p-[2px]"
4158
style={{
4259
background: open
@@ -70,18 +87,24 @@ export default function UserAvatar() {
7087

7188
<AnimatePresence>
7289
{open && (
90+
<FloatingPortal>
91+
<div
92+
ref={refs.setFloating}
93+
style={floatingStyles}
94+
{...getFloatingProps()}
95+
className="z-[9999]"
96+
>
7397
<motion.div
74-
initial={{ opacity: 0, y: 10, scale: 0.92 }}
98+
initial={{ opacity: 0, y: 8, scale: 0.95 }}
7599
animate={{ opacity: 1, y: 0, scale: 1 }}
76-
exit={{ opacity: 0, y: 10, scale: 0.92 }}
100+
exit={{ opacity: 0, y: 8, scale: 0.95 }}
77101
transition={{ duration: 0.2, ease: [0.22, 1, 0.36, 1] }}
78102
className={cn(
79-
"absolute right-0 top-full mt-3 w-60",
103+
"w-60",
80104
"rounded-2xl overflow-hidden",
81105
"bg-surface/95 backdrop-blur-2xl",
82106
"border border-white/[0.06]",
83107
"shadow-[0_16px_48px_rgba(0,0,0,0.5),0_0_1px_rgba(255,255,255,0.05)]",
84-
"z-50"
85108
)}
86109
>
87110
{/* Prismatic top accent line */}
@@ -167,6 +190,8 @@ export default function UserAvatar() {
167190
</button>
168191
</div>
169192
</motion.div>
193+
</div>
194+
</FloatingPortal>
170195
)}
171196
</AnimatePresence>
172197
</div>

src/components/Common/Navbar/Navbar.tsx

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,18 @@ const MobileNavLink = ({
228228
mmFont?: string;
229229
}) => (
230230
<motion.div
231-
initial={{ opacity: 0, x: -40, filter: "blur(8px)" }}
232-
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
233-
exit={{ opacity: 0, x: 40, filter: "blur(8px)" }}
231+
initial={{ opacity: 0, x: -20 }}
232+
animate={{ opacity: 1, x: 0 }}
233+
exit={{ opacity: 0, x: 20 }}
234234
transition={{
235-
delay: 0.08 + index * 0.1,
236-
duration: 0.5,
235+
delay: 0.05 + index * 0.06,
236+
duration: 0.35,
237237
ease: [0.22, 1, 0.36, 1],
238238
}}
239239
>
240240
<MseLink
241241
href={href}
242-
className="group relative block py-3"
242+
className="group relative block py-2 sm:py-3"
243243
>
244244
<span onClick={onClick} className="flex items-center gap-4">
245245
{/* Index number */}
@@ -250,7 +250,7 @@ const MobileNavLink = ({
250250
{/* Link label */}
251251
<span
252252
className={cn(
253-
"font-display text-4xl sm:text-5xl font-bold tracking-tight transition-all duration-300",
253+
"font-display text-[22px] xs:text-3xl sm:text-5xl font-bold tracking-tight transition-all duration-300",
254254
isActive
255255
? "bg-prism-gradient bg-clip-text text-transparent"
256256
: "text-zinc-400 group-hover:text-white",
@@ -267,20 +267,14 @@ const MobileNavLink = ({
267267
className="w-2 h-2 rounded-full"
268268
style={{
269269
background: "linear-gradient(135deg, #22d3ee, #fb7185)",
270-
boxShadow: "0 0 12px rgba(167,139,250,0.6)",
271270
}}
272271
transition={{ type: "spring", stiffness: 400, damping: 25 }}
273272
/>
274273
)}
275274
</span>
276275

277-
{/* Hover line reveal */}
278-
<motion.div
279-
className="absolute bottom-0 left-8 right-0 h-[1px] bg-white/5 origin-left"
280-
initial={{ scaleX: 0 }}
281-
animate={{ scaleX: 1 }}
282-
transition={{ delay: 0.3 + index * 0.1, duration: 0.6 }}
283-
/>
276+
{/* Divider line */}
277+
<div className="absolute bottom-0 left-8 right-0 h-[1px] bg-white/5" />
284278
</MseLink>
285279
</motion.div>
286280
);
@@ -419,50 +413,29 @@ const Navbar = () => {
419413
exit={{ opacity: 0 }}
420414
transition={{ duration: 0.3 }}
421415
>
422-
{/* Backdrop with gradient */}
423-
<motion.div
416+
{/* Backdrop */}
417+
<div
424418
className="absolute inset-0"
425419
style={{
426420
background:
427421
"radial-gradient(ellipse at top right, rgba(167,139,250,0.08), transparent 50%), radial-gradient(ellipse at bottom left, rgba(34,211,238,0.05), transparent 50%), #09090bf5",
428422
}}
429-
initial={{ opacity: 0 }}
430-
animate={{ opacity: 1 }}
431-
exit={{ opacity: 0 }}
432423
/>
433424

434425
{/* Decorative grid lines */}
435426
<div className="absolute inset-0 overflow-hidden pointer-events-none">
436-
<motion.div
437-
className="absolute top-0 left-8 w-[1px] h-full bg-white/[0.03]"
438-
initial={{ scaleY: 0 }}
439-
animate={{ scaleY: 1 }}
440-
transition={{ delay: 0.2, duration: 0.8, ease: "easeOut" }}
441-
style={{ transformOrigin: "top" }}
442-
/>
443-
<motion.div
444-
className="absolute top-0 right-8 w-[1px] h-full bg-white/[0.03]"
445-
initial={{ scaleY: 0 }}
446-
animate={{ scaleY: 1 }}
447-
transition={{ delay: 0.3, duration: 0.8, ease: "easeOut" }}
448-
style={{ transformOrigin: "top" }}
449-
/>
427+
<div className="absolute top-0 left-6 sm:left-8 w-[1px] h-full bg-white/[0.03]" />
428+
<div className="absolute top-0 right-6 sm:right-8 w-[1px] h-full bg-white/[0.03]" />
450429
</div>
451430

452431
{/* Menu content */}
453-
<div className="relative h-full flex flex-col justify-center px-8 sm:px-12">
432+
<div className="relative h-full flex flex-col justify-center px-6 sm:px-12">
454433
{/* Section label */}
455-
<motion.div
456-
className="mb-8"
457-
initial={{ opacity: 0, y: 10 }}
458-
animate={{ opacity: 1, y: 0 }}
459-
exit={{ opacity: 0 }}
460-
transition={{ delay: 0.15 }}
461-
>
434+
<div className="mb-8">
462435
<span className={cn("font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600", mmFont)}>
463436
{t("navigation")}
464437
</span>
465-
</motion.div>
438+
</div>
466439

467440
{/* Links */}
468441
<nav className="flex flex-col gap-2">
@@ -480,12 +453,7 @@ const Navbar = () => {
480453
</nav>
481454

482455
{/* Bottom section — auth + branding */}
483-
<motion.div
484-
className="absolute bottom-12 left-8 right-8"
485-
initial={{ opacity: 0 }}
486-
animate={{ opacity: 1 }}
487-
transition={{ delay: 0.6 }}
488-
>
456+
<div className="absolute bottom-12 left-6 right-6 sm:left-8 sm:right-8">
489457
{/* Auth */}
490458
{!authLoading && (
491459
<div className="mb-5">
@@ -504,7 +472,7 @@ const Navbar = () => {
504472
</p>
505473
<LanguageToggle />
506474
</div>
507-
</motion.div>
475+
</div>
508476
</div>
509477
</motion.div>
510478
)}

tailwind.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const config: Config = {
1111
"./src/utils/**/*.{ts, tsx}",
1212
],
1313
theme: {
14+
screens: {
15+
xs: "375px",
16+
sm: "640px",
17+
md: "768px",
18+
lg: "1024px",
19+
xl: "1280px",
20+
"2xl": "1536px",
21+
},
1422
extend: {
1523
fontFamily: {
1624
display: ["var(--font-display)", "sans-serif"],

0 commit comments

Comments
 (0)