Skip to content

Commit 44327e9

Browse files
committed
feat(nav): add floating pill navigation with scroll-direction trigger
- new FloatingNav: hides past hero, re-appears on scroll-up - mounts alongside the static hero header so branding stays at top - scroll-margin-top on section anchors so jumps clear the floating nav
1 parent e493c56 commit 44327e9

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

app/globals.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ html {
5757
scroll-behavior: smooth;
5858
}
5959

60+
section[id] {
61+
scroll-margin-top: 80px;
62+
}
63+
6064
@media (prefers-reduced-motion: no-preference) and (min-width: 1024px) {
6165
html {
6266
scroll-snap-type: y proximity;

app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Header } from "@/components/site/header";
2+
import { FloatingNav } from "@/components/site/floating-nav";
23
import { Hero } from "@/components/site/hero";
34
import { CompanyIntro } from "@/components/site/company-intro";
45
import { SolutionsGrid } from "@/components/site/solutions-grid";
@@ -11,6 +12,7 @@ export default function Home() {
1112
return (
1213
<>
1314
<Header />
15+
<FloatingNav />
1416
<main className="flex-1">
1517
<Hero />
1618
<CompanyIntro />

components/site/floating-nav.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Image from "next/image";
5+
import {
6+
AnimatePresence,
7+
motion,
8+
useMotionValueEvent,
9+
useReducedMotion,
10+
useScroll,
11+
} from "motion/react";
12+
13+
const NAV_ITEMS = [
14+
{ href: "#about", label: "About" },
15+
{ href: "#oma", label: "Engineering" },
16+
{ href: "#solutions", label: "Solution" },
17+
{ href: "#contact", label: "Contact" },
18+
];
19+
20+
const EASE_OUT_EXPO = [0.22, 1, 0.36, 1] as const;
21+
22+
export function FloatingNav() {
23+
const reduceMotion = useReducedMotion();
24+
const { scrollY } = useScroll();
25+
const [visible, setVisible] = useState(false);
26+
27+
useMotionValueEvent(scrollY, "change", (current) => {
28+
const previous = scrollY.getPrevious() ?? 0;
29+
if (current < 80) {
30+
setVisible(false);
31+
} else if (current < previous) {
32+
setVisible(true);
33+
} else {
34+
setVisible(false);
35+
}
36+
});
37+
38+
return (
39+
<AnimatePresence>
40+
{visible && (
41+
<motion.nav
42+
aria-label="섹션 네비게이션"
43+
initial={reduceMotion ? { opacity: 0 } : { y: -80, opacity: 0 }}
44+
animate={reduceMotion ? { opacity: 1 } : { y: 0, opacity: 1 }}
45+
exit={reduceMotion ? { opacity: 0 } : { y: -80, opacity: 0 }}
46+
transition={{ duration: 0.32, ease: EASE_OUT_EXPO }}
47+
className="fixed inset-x-0 top-4 z-40 mx-auto flex w-fit max-w-[calc(100vw-1.5rem)] items-center gap-1 rounded-full border border-[var(--color-border)] bg-white/85 px-3 py-2 shadow-[0_8px_30px_rgba(15,76,58,0.08)] backdrop-blur-md md:gap-2 md:px-5 md:py-2.5"
48+
>
49+
<a
50+
href="#top"
51+
aria-label="맨 위로"
52+
className="mr-1 inline-flex shrink-0 items-center gap-1.5 rounded-full px-1.5 py-0.5 text-[var(--color-primary)] md:mr-2"
53+
>
54+
<Image
55+
src="/logo.png"
56+
alt=""
57+
width={24}
58+
height={24}
59+
unoptimized
60+
className="h-6 w-6"
61+
/>
62+
<span className="hidden text-[13px] font-bold tracking-tight sm:inline">
63+
FIRST FLUKE
64+
</span>
65+
</a>
66+
<span
67+
aria-hidden
68+
className="hidden h-4 w-px bg-[var(--color-border)] sm:inline-block"
69+
/>
70+
{NAV_ITEMS.map((item) => (
71+
<a
72+
key={item.href}
73+
href={item.href}
74+
className="rounded-full px-2.5 py-1 text-[13px] font-medium text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-primary)] md:px-3 md:text-sm"
75+
>
76+
{item.label}
77+
</a>
78+
))}
79+
</motion.nav>
80+
)}
81+
</AnimatePresence>
82+
);
83+
}

0 commit comments

Comments
 (0)