9.3 KiB

Component Patterns

Hydration Strategy

Directive When to Use Components
client:load Above-fold, needs immediate interaction Navigation, Hero, Loader, CustomCursor
client:visible Below-fold, can wait until scrolled into view About, Contact, Footer, GamesList, Marquee
client:idle Low priority, background loading BlogSearch

GSAP Animation Patterns

Required Setup in Every Animated Component

import { useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export default function AnimatedComponent() {
  const sectionRef = useRef<HTMLElement>(null);

  useEffect(() => {
    // Always check reduced motion
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

    // Always wrap in context for cleanup
    const ctx = gsap.context(() => {
      // animations here
    });

    return () => ctx.revert();
  }, []);
}

SessionStorage One-Time Entrance

For Hero and Loader — skip expensive animations on repeat visits:

const [isVisible, setIsVisible] = useState(() => {
  // Synchronous check before first render
  if (typeof window === 'undefined') return true;
  return !sessionStorage.getItem('{{STORAGE_KEY}}');
});

// In animation completion callback:
sessionStorage.setItem('{{STORAGE_KEY}}', 'true');
setIsVisible(false);

ScrollTrigger Entrance Pattern

For below-fold sections (About, Contact, GamesList):

gsap.context(() => {
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: sectionRef.current,
      start: 'top 80%',
      toggleActions: 'play none none none',
    },
  });

  tl.from(leftRef.current, {
    x: -50, opacity: 0, duration: 0.8, ease: 'power2.out',
  })
  .from(rightRef.current, {
    x: 50, opacity: 0, duration: 0.8, ease: 'power2.out',
  }, '-=0.6');  // Overlap by 0.6s for flowing feel
});

Parallax Pattern

gsap.to(bgRef.current, {
  yPercent: 20,
  ease: 'none',
  scrollTrigger: {
    trigger: sectionRef.current,
    start: 'top bottom',
    end: 'bottom top',
    scrub: true,
  },
});

Idle Float Animation

gsap.to(elementRef.current, {
  y: 12,
  rotation: 1.5,
  duration: 3,
  ease: 'sine.inOut',
  yoyo: true,
  repeat: -1,
});

Animation Rules

  • Transform-only: Use x, y, scale, rotation, opacity. Never animate width, height, top, left, margin, padding.
  • Stagger: Use negative relative positioning ('-=0.6') in timelines for overlapping reveals.
  • Ease functions: power2.out for entrances, power3.inOut for exits, sine.inOut for idle, back.out(1.7) for bouncy CTAs.

Navigation Component

Fixed header + fullscreen overlay menu:

Header Bar:

  • Fixed position, z-50
  • Transparent when at top, semi-opaque after scroll (useEffect with scroll listener)
  • Logo (left), optional center ticker, controls (right: audio toggle, menu button)
  • Top accent line: h-[2px] gradient with brand colors

Fullscreen Menu:

  • Hidden by default (display: none), shown via GSAP
  • Background: layered gradients, noise texture, scanlines
  • Two-column: nav links (left) + info (right)
  • Nav links: indexed (01-06), description on hover (hidden mobile)
  • GSAP open sequence: scanline wipe → bg fade → links stagger → info slide
  • GSAP close: all fade 0.15s → bg fade 0.2s → display: none

Interactions:

  • ESC to close (KeyboardEvent listener)
  • Body overflow hidden when open
  • Hash links scroll with GSAP ScrollToPlugin
  • Audio SFX via Web Audio API (optional — synthesized, no files needed)
  • 44px minimum touch targets on all links/buttons

Hero Component

Full viewport section:

Background Layers (bottom to top):

  1. Video/image (opacity 50%, object-cover)
  2. Gradient overlays (bottom-to-top dark, radial center-fade)
  3. Noise texture (mix-blend-overlay, opacity 3-5%)
  4. Scanlines (optional)

Content:

  • Centered: logo, tagline, 2 CTA buttons, optional stats ticker
  • Floating decorative elements (characters, icons) with idle float animations
  • Diagonal accent slash (optional, brand colored)

Entrance Animation (sessionStorage skip on repeat):

0.0s: Start
0.3s: Slash scaleX 0→1 (0.6s)
0.5s: Logo blur(20px) x:-120 → clear x:0 (1.0s)
0.7s: Character blur → clear (0.9s)
1.0s: Tagline clipPath reveal (0.7s)
1.2s: CTAs scale+y stagger (0.6s each, 0.12s gap)
1.5s: Ticker fade up (0.6s)

Parallax (always active):

  • Video: yPercent +20 on scroll
  • Character: yPercent -30 on scroll

Loader Component (optional)

One-time splash screen:

const [isVisible, setIsVisible] = useState(() => {
  if (typeof window === 'undefined') return true;
  return !sessionStorage.getItem('{{KEY}}-loader-seen');
});

if (!isVisible) return null;
  • Progress bar: width 0→100% (1.5s)
  • Logo: opacity 0, scale 1.1, blur 10px → visible (0.5s)
  • Container: yPercent 0→-100 (0.8s, power3.inOut)
  • Safety timeout: 5s max
  • Click anywhere to skip
  • On complete: sessionStorage.setItem, setIsVisible(false)

CustomCursor Component (optional)

export default function CustomCursor() {
  const cursorRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (window.matchMedia('(pointer: coarse)').matches) return;

    const onMove = (e: MouseEvent) => {
      gsap.to(cursorRef.current, {
        x: e.clientX, y: e.clientY,
        duration: 0.1, ease: 'power2.out',
      });
    };

    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  }, []);

  return <div ref={cursorRef} id="custom-cursor" />;
}

Styled via global.css #custom-cursor rules.

Contact Component

Two-column layout:

Left Column:

  • Section label (accent color, pixel font)
  • Heading with gradient text on key word
  • Contact email link
  • Social icons row with hover color transitions
  • Optional location tagline

Right Column (Form):

  • Fields: name, email, subject, message
  • Honeypot: _honey (hidden, aria-hidden, tabIndex -1)
  • Submit button: brand bg, white text, pixel shadow, hover translate 2px
  • Status states: idle → sending (disabled) → success (cyan border box) → error (primary border box)

GSAP entrance: ScrollTrigger, left x:-50, right x:+50, staggered.

Multi-column grid:

┌─────────────────────────────────────────────────────────┐
│  Brand       │  SITEMAP      │  LEGAL        │  CTA    │
│  Logo        │  Home         │  Privacy      │  Steam  │
│  Name        │  Product      │  Terms        │  Button │
│  Tagline     │  About        │  Press        │         │
│  Socials     │  Blog         │               │         │
│              │  Contact      │               │         │
├─────────────────────────────────────────────────────────┤
│  © 2024 Company. Est. 20XX.                             │
└─────────────────────────────────────────────────────────┘
  • Background: --color-darker (#050505)
  • Text: white/35 base, white on hover, 200ms transition
  • Grid: 1 col mobile → 2 cols tablet → 4 cols desktop

External API Integration Pattern

Build-time data fetching (e.g., Steam API):

// src/lib/steam.ts
interface SteamData {
  app: SteamAppDetails | null;
  reviews: SteamReviewSummary | null;
  players: number | null;
  news: SteamNewsItem[];
  version: string | null;
}

async function fetchJson<T>(url: string): Promise<T | null> {
  try {
    const res = await fetch(url);
    if (!res.ok) return null;
    return await res.json() as T;
  } catch {
    return null;
  }
}

export async function fetchAllData(id: number): Promise<SteamData> {
  const [app, reviews, players, news] = await Promise.all([
    fetchAppDetails(id),
    fetchReviewSummary(id),
    fetchCurrentPlayers(id),
    fetchNews(id),
  ]);

  return { app, reviews, players, news, version: parseVersionFromNews(news) };
}

Key principles:

  • Every fetch returns T | null — never throws
  • Use Promise.all for parallel fetching
  • Aggregate into single data object with nullable fields
  • Builds never break if external API is down
  • Wire into pages via frontmatter await

Accessibility Checklist

Every component should have:

  • aria-label on icon-only buttons
  • 44px minimum touch targets (w-11 h-11, or py-3 px-3 -mx-3)
  • Keyboard navigation (ESC to close overlays, Tab order)
  • focus-visible outline styles
  • Semantic HTML (<nav>, <article>, <footer>, <section id="...">)
  • Color contrast meeting WCAG AA on dark backgrounds

Mobile Patterns

  • Touch feedback: :active scale 0.985, 80ms transition
  • Image bleed to edges on mobile (-mx-5, max-width: calc(100% + 2.5rem))
  • Horizontal scroll with overflow-x-auto scrollbar-none for category strips
  • Safe area: padding-bottom: calc(5rem + env(safe-area-inset-bottom))
  • pointer: coarse media query to hide desktop-only features (custom cursor, hover descriptions)