NavBar Component

By Jay Griffin, Claude Sonnet 4.5*Collaboratively written with Claude by summarizing work done and analyzing the code·  February 3, 2026
docsFeature: navbar
🏷️ Tags:componentsnavigationanimationmobile

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

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:

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.

NavBar.tsx (simplified)
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

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.

The fix
// 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

Lessons Learned

Technical Implementation Details

The NavBar component maintains several pieces of state to manage the particle system:

.ts
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);

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:

Responsive Behavior

On mobile (viewport ≤ 768px), the navbar adapts in several ways:

Future Improvements

Possible enhancements for the navbar:

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.