NavBar Component
February 4, 2026
The fixed navigation bar that lives at the top of every page, featuring an animated title, search navigator, and menu system with elegant mobile responsiveness.
NavBar Component
The NavBar is the primary navigation component that appears at the top of every page. It provides quick access to search, menu navigation, and features an interactive animated title that serves as the home link.
Overview
The navbar uses a fixed positioning strategy, staying visible at the top of the viewport as you scroll. It has a floating pill design with a dark background, subtle border, and shadow effects that give it depth and separation from the page content.
Core Features
- Fixed positioning - Stays at top of viewport (1.5rem from top on desktop, 0 on mobile)
- Floating pill design - Rounded edges, backdrop blur, subtle border and shadow
- Interactive title - "Jay Griffin" text with hover gradient effect and particle animations
- Navigator integration - Search/filter component for quick page access
- Menu system - Hamburger menu with all site navigation
- Responsive design - Adapts sizing and spacing for mobile devices
Layout and Positioning
The navbar was originally positioned at the bottom of the screen but was moved to the top for better usability. This required several cascading changes including body padding (6rem on desktop, 4rem on mobile) to prevent content from being hidden underneath, and updating the Navigator and NavMenu popovers to open downward instead of upward.
The component uses position: fixed rather than position: sticky because the latter caused annoying elastic scroll behavior on macOS. The tradeoff is requiring explicit body padding, but this provides a more consistent experience across devices.
Child Components
The navbar orchestrates three main components:
- NavMenu - Dropdown menu with links to posts, docs, commits, and other sections. Uses Radix UI DropdownMenu with
modal=falseto prevent body scroll lock. Closes automatically on scroll. - Title Link - The "Jay Griffin" text that links to the homepage. Features hover gradient and particle effects. Uses the Sekuya font for distinctive styling.
- Navigator - Popover with search functionality for filtering and navigating to pages. Auto-focuses search input on desktop but not mobile (to prevent keyboard popup). Also closes on scroll.
Particle Effect Implementation
One of the most distinctive features of the navbar is the animated particle effect on the title. When you hover over "Jay Griffin", small cyan particles spawn and float outward in random directions, creating a dynamic, playful effect that complements the gradient color shift.
How It Works
The particle system uses an interval that generates new particles every 150ms while hovering. Each particle floats outward in a random direction, fading out over 1.5 seconds before being removed from the DOM.
const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
setIsHovered(true);
isHoveredRef.current = true;
const rect = e.currentTarget.getBoundingClientRect();
intervalRef.current = setInterval(() => {
const x = Math.random() * rect.width;
const y = Math.random() * rect.height;
const angle = Math.random() * Math.PI * 2;
const distance = 40 + Math.random() * 40;
const particle = {
id: particleIdRef.current++,
x,
y,
tx: Math.cos(angle) * distance,
ty: Math.sin(angle) * distance,
};
// Only add if still hovering
setParticles(prev => {
if (!isHoveredRef.current) return prev;
return [...prev, particle];
});
setTimeout(() => {
setParticles(prev => prev.filter(p => p.id !== particle.id));
}, 1500);
}, 150);
};The Particle Synchronization Challenge
While implementing the particle effects, I ran into a deceptively difficult problem: keeping the particles perfectly synchronized with the hover state. My requirement was simple - particles should only exist when the title is highlighted. If there's no gradient, there should be no particles. They should be logically intertwined, making it physically impossible for one to exist without the other.
This turned out to be much harder than expected because of subtle timing issues between CSS :hover state and JavaScript mouse events, particularly on mobile devices.
Failed Attempts
- Touch event handlers - Added
onTouchStart,onTouchEnd,onTouchMovewith boundary detection. Problem: mobile browsers don't fire touch move events during scrolling, so particles continued when I scrolled away from the title. - Scroll listeners - Added a window scroll listener to stop particles when scrolling. Problem: caused buggy behavior, particles would stop during normal page scrolling even when not touching the title.
- Complex touch boundary checking - Used
getBoundingClientRect()to check if touch coordinates were still within the element. Problem: either fired incorrectly or didn't fire at all during certain gestures.
The Solution: Ref-Based State Tracking
The breakthrough was using a ref to track hover state that the interval closure could access in real-time. Here's the key insight: the interval's callback captures variables at creation time, so checking a state variable won't work - it always sees the initial value.
// Both state (for UI) and ref (for interval closure)
const [isHovered, setIsHovered] = useState(false);
const isHoveredRef = useRef(false);
const handleMouseEnter = (e: React.MouseEvent<HTMLAnchorElement>) => {
setIsHovered(true);
isHoveredRef.current = true; // Update ref immediately
// ... start interval
};
const handleMouseLeave = () => {
setIsHovered(false);
isHoveredRef.current = false; // Update ref immediately
// Stop interval and clear particles
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setParticles([]); // Clear all particles immediately
};
// Inside the interval
setParticles(prev => {
if (!isHoveredRef.current) return prev; // Check ref, not state!
return [...prev, particle];
});Why This Works
isHoveredRef.currentis checked before adding each particle, making it physically impossible to add particles when not hoveringsetParticles([])inhandleMouseLeaveimmediately clears all existing particles, so there's no fade-out delay- Simple mouse events only - no complex touch handling. Browsers naturally convert touch events to mouse events, and the hover state tracking handles all edge cases
- The CSS
:hoverand JavaScript hover state stay perfectly synchronized because they both respond to the same underlying mouse/touch events
Lessons Learned
- Simple is better - the final solution removed all complex touch handling and just trusts the browser's mouse event abstraction
- Interval closures capture variables at creation time - use refs for values that need to be checked in real-time
- Mobile scrolling suppresses touch move events - can't rely on
onTouchMovefor tracking - When synchronizing visual effects with hover state, clearing state immediately on mouse leave is crucial for maintaining the illusion of tight coupling
- Sometimes the best mobile solution is to let the browser handle touch-to-mouse event conversion rather than trying to handle touch events explicitly
Technical Implementation Details
The NavBar component maintains several pieces of state to manage the particle system:
const [particles, setParticles] = useState<Particle[]>([]);
const [isHovered, setIsHovered] = useState(false);
const isHoveredRef = useRef(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const particleIdRef = useRef(0);particles- Array of active particles with position and velocity dataisHovered- State for potential future UI needsisHoveredRef- Ref for real-time hover checking in interval closureintervalRef- Reference to the active particle generation intervalparticleIdRef- Counter for unique particle IDs
The particles are rendered as absolutely positioned divs with a CSS animation that fades them out while moving them outward. Each particle knows its starting position and travel vector, and CSS custom properties handle the animation.
Styling and Theme Integration
The navbar uses Emotion for styling with a combination of static styles and responsive breakpoints. Key design elements include:
- Background - Dark (#1a1a24) with subtle transparency and backdrop blur
- Border - White with 10% opacity plus a shadow for depth
- Typography - Sekuya font at 2.5rem (clamps down to 1.25rem on mobile)
- Hover gradient - Linear gradient from cyan (#00d4ff) to navy blue (#1e3a8a)
- Particles - 3px cyan dots with CSS keyframe animation
Responsive Behavior
The navbar went through a significant layout refactor to achieve perfect centering with responsive behavior. Initially attempted with absolute positioning (which caused collision issues), the final solution uses a clean three-column flexbox architecture.
Current Layout Architecture
The navbar uses display: flex with justifyContent: space-between to create three distinct columns:
- Left Controls - Fixed-width column (
flex: 0 0 auto) containing the hamburger menu and search icons (~84px total) - Title Container - Growing center column (
flex: 1 1 auto) that centers the "Jay Griffin" title viajustifyContent: center - Right Spacer - Fixed-width empty column (
flex: 0 0 auto, width: 84px) that matches the left controls width for perfect symmetry
This architecture naturally prevents element collisions - when the viewport shrinks, the flex container automatically manages space distribution without elements overlapping.
Responsive Breakpoints
At 550px and below, the navbar applies two simultaneous changes:
- Font resize - Title shrinks from
2.5remto2rem - Layout shift - Title container changes to
justifyContent: flex-end(right-aligned) and the right spacer hides (display: none)
The 550px breakpoint targets the "dead zone" - larger than phones in portrait (~430px max) but smaller than tablets (768px+). This catches landscape phones, narrow browser windows, and very small tablets.
Design Philosophy: 90% Coverage
The responsive strategy deliberately targets modern devices and practical viewport sizes. The layout works perfectly down to 375px (iPhone SE), which covers the vast majority of mobile users in 2026. Below ~355px, the layout may break slightly, but these edge cases represent ancient devices with negligible market share.
As I said during development: "I'm all about doing half the work for capturing 90% of the use cases. If you're on Internet Explorer, go throw your computer in the trash. If you're on iPhone 5, you're a walking security risk."
What We Removed
The final implementation is significantly simpler than earlier attempts. Removed features include:
- Absolute positioning - Initially tried to center the title with
position: absolute, left: 50%, transform: translateX(-50%)but this caused height collapse and collision issues - Pointer events workarounds - The absolute positioned title blocked clicks, requiring
pointerEvents: nonepatches - Z-index stacking fixes - Absolute positioning created overlay issues that needed z-index management
- Text overflow ellipsis - Briefly tried truncating the name ("Jay Griff...") which was immediately rejected as it felt wrong for personal branding
- Complex responsive font scaling - Originally used
clamp(1rem, 5vw, 2.5rem)for fluid font scaling, but the simpler two-size approach (2.5rem β 2rem at 550px) is cleaner - Nested container divs - The old
navBarContentStyleswrapper is gone; the nav element itself is now the flex container
Mobile-Specific Behaviors
Beyond the 550px breakpoint, mobile devices get these optimizations:
- Navbar sits at the very top of the screen (top: 0)
- Smaller icon buttons (maintain usability with reduced size)
- Navigator search input doesn't auto-focus (prevents unwanted keyboard popup)
- iOS tap highlights disabled (
-webkit-tap-highlight-color: transparent)
Future Improvements
Possible enhancements for the navbar:
- Add visual indicator for current page/section
- Experiment with different particle shapes or colors based on theme
- Add keyboard shortcuts for opening Navigator (βK style)
- Consider adding breadcrumb navigation for deeper page hierarchies
Related Components
The navbar integrates closely with Navigator and NavMenu components. It also works in concert with the global body padding defined in GlobalStyles to ensure content isn't hidden underneath the fixed positioning.
Related Posts
- Container & Margin Behavior MapA detailed map of how my global body spacing, Container primitive margins, and page-level overrides combine across the site.
- Building the Navigator: A Route Search ToolA complete walkthrough of the Navigator feature - a popover search tool for quickly finding and navigating to any route in the application.