NavBar Component

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

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

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:

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:

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:

Mobile-Specific Behaviors

Beyond the 550px breakpoint, mobile devices get these optimizations:

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.

Related Posts

  • Container & Margin Behavior Map
    A 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 Tool
    A complete walkthrough of the Navigator feature - a popover search tool for quickly finding and navigating to any route in the application.