NavBar Component
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
On mobile (viewport ≤ 768px), the navbar adapts in several ways:
- Moves to the very top of the screen (top: 0)
- Title font size uses clamp() for fluid scaling
- Reduced padding on the title (0.15rem vs 0.2rem)
- Smaller icon buttons (36px vs 40px)
- Navigator search input doesn't auto-focus (prevents keyboard popup)
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.