9.3 KiB
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.outfor entrances,power3.inOutfor exits,sine.inOutfor 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):
- Video/image (opacity 50%, object-cover)
- Gradient overlays (bottom-to-top dark, radial center-fade)
- Noise texture (mix-blend-overlay, opacity 3-5%)
- 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.
Footer Component
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.allfor 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-labelon 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-visibleoutline styles- Semantic HTML (
<nav>,<article>,<footer>,<section id="...">) - Color contrast meeting WCAG AA on dark backgrounds
Mobile Patterns
- Touch feedback:
:activescale 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-nonefor category strips - Safe area:
padding-bottom: calc(5rem + env(safe-area-inset-bottom)) pointer: coarsemedia query to hide desktop-only features (custom cursor, hover descriptions)