NavigationSimpleApril 9, 2026
Nav Bar
Fixed transparent navbar with name/role on the left, flip-text navigation links in the center, and a live EST clock on the right. Hover reveals a growing underline and a vertical text flip. Active link state is driven by URL hash.
View Full Demo →Preview
Source
demo.jsx
import NavBar from "./index.jsx";
import styles from "./demo.module.css";
export default function NavBarDemo() {
return (
<div className={styles.demo}>
<NavBar />
<div className={styles.hero}>
<p className={styles.label}>Est. 2024 — New York</p>
<h1 className={styles.heading}>No. 5 Studio</h1>
<p className={styles.sub}>Design & Development</p>
</div>
<div className={styles.footer}>
<span>Selected Work</span>
<span>Currently Available</span>
</div>
</div>
);
}
index.jsx
'use client';
import { useState, useEffect } from "react";
import Link from "next/link";
import styles from "./styles.module.css";
export default function NavBar({ name = "Kevin Davis", role = "Web Developer", navItems = ["Work", "About", "Contact"] }) {
const [time, setTime] = useState("");
const [activeHash, setActiveHash] = useState("");
// --- Clock logic ---
useEffect(() => {
const updateClock = () => {
const now = new Date();
const options = {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
timeZone: "America/New_York",
};
const formatted = now.toLocaleTimeString("en-US", options).replace(" ", " ");
setTime(formatted);
};
updateClock();
const timer = setInterval(updateClock, 1000);
return () => clearInterval(timer);
}, []);
// --- Hash tracking for active underline ---
useEffect(() => {
const handleHashChange = () => setActiveHash(window.location.hash);
window.addEventListener("hashchange", handleHashChange);
handleHashChange();
return () => window.removeEventListener("hashchange", handleHashChange);
}, []);
return (
<nav className={styles.navbar}>
{/* Left: Name and role */}
<div className={styles.left}>
<h3 className={styles.name}>{name}</h3>
<span className={styles.role}>{role}</span>
</div>
{/* Middle: Navigation links */}
<ul className={styles.links}>
{navItems.map((item) => {
const hash = `#${item.toLowerCase()}`;
return (
<li key={item}>
<Link
href={hash}
onClick={() => setActiveHash(hash)}
className={`${styles.btnLineHover} ${activeHash === hash ? styles.active : ""}`}
>
<span className={styles.btnText}>
<span>{item}</span>
<span>{item}</span>
</span>
</Link>
</li>
);
})}
</ul>
{/* Right: Live EST Clock */}
<div className={styles.clock}>
{time && <span> {time} EST</span>}
</div>
</nav>
);
}
demo.module.css
.demo {
position: relative;
min-height: 100vh;
background: #0a0a0a;
display: flex;
flex-direction: column;
/* Contains fixed-position children (NavBar) within this element */
transform: translateZ(0);
}
.hero {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 6rem 2rem 4rem;
gap: 1rem;
}
.label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.35);
}
.heading {
font-size: clamp(3.5rem, 10vw, 9rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 0.95;
color: #ffffff;
}
.sub {
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.4);
}
.footer {
display: flex;
justify-content: space-between;
padding: 1.5rem 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.25);
}
styles.module.css
.navbar {
--fg: #ffffff;
position: fixed;
top: 1.25rem;
left: 2rem;
right: 2rem;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
z-index: 1000;
opacity: 0;
animation: fadeIn 0.8s ease forwards;
color: var(--fg);
font-weight: 600;
letter-spacing: 0.01em;
background: transparent;
}
/* left: name + role */
.left {
display: flex;
align-items: baseline;
gap: 2rem;
}
.name,
.role {
font-size: 1.05rem;
font-weight: 400;
color: var(--fg);
opacity: 0.8;
}
/* middle: nav links */
.links {
display: flex;
justify-content: center;
gap: .5rem;
list-style: none;
margin: 0;
padding-left: 30rem;
}
.links a {
text-decoration: none;
color: var(--fg);
font-size: 1.1rem;
font-weight: 400;
transition: opacity 0.2s ease;
}
/* ───────────────────────────────
Flip + underline
─────────────────────────────── */
.btnLineHover {
position: relative;
display: inline-block;
overflow: visible;
color: var(--fg);
text-decoration: none;
transition: color 0.3s ease;
padding-bottom: 3px;
}
/* underline: single line grows left → right */
.btnLineHover::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background-color: var(--fg);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
/* reveal underline on hover */
.btnLineHover:hover::after {
transform: scaleX(1);
}
/* stacked text container */
.btnText {
display: inline-block;
position: relative;
overflow: hidden;
line-height: 1.2;
}
/* both spans (top + bottom) occupy same space */
.btnText span {
display: block;
transition: transform 0.45s cubic-bezier(0.77, 0, 0.175, 1),
opacity 0.45s ease;
}
/* hover: top text goes up/fades, bottom text slides in */
.btnLineHover:hover .btnText span:first-child {
transform: translateY(-100%);
opacity: 0;
}
.btnLineHover:hover .btnText span:last-child {
transform: translateY(-100%);
opacity: 1;
}
/* start: bottom text hidden below */
.btnText span:last-child {
position: absolute;
left: 0;
top: 100%;
opacity: 0;
}
/* right: live clock */
.clock {
font-size: 1.1rem;
font-weight: 400;
text-transform: uppercase;
white-space: nowrap;
text-align: right;
}
/* keep underline visible on active link */
.btnLineHover.active::after {
transform: scaleX(1);
}
/* fade-in animation for navbar */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}