tsx
GSAP reveal on scroll (context-safe)
Stagger content in using GSAP + ScrollTrigger inside a context so it reverts cleanly.
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
export function Reveal({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const ctx = gsap.context(() => {
gsap.fromTo(el,
{ y: 48, autoAlpha: 0 },
{ y: 0, autoAlpha: 1, duration: 1, ease: "power3.out",
scrollTrigger: { trigger: el, start: "top 88%", once: true } });
}, el);
return () => ctx.revert();
}, []);
return <div ref={ref}>{children}</div>;
}GSAPScrollTrigger
ts
Lenis smooth scroll + GSAP ticker
Keep smooth scrolling in sync with GSAP's ScrollTrigger.
import Lenis from "lenis";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis({ duration: 1.15, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));
gsap.ticker.lagSmoothing(0);
// call lenis.destroy() on unmountLenisGSAP
ts
Respect prefers-reduced-motion
Gate heavy animation behind the user's motion preference.
const REDUCED =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!REDUCED) {
gsap.to(el, { x: 100, duration: 1 });
} else {
gsap.set(el, { x: 0, autoAlpha: 1 }); // skip straight to final state
}inclusivemotion
Visible keyboard focus without ugly outlines on mouse clicks.
:focus-visible {
outline: 2px solid #f97316;
outline-offset: 3px;
border-radius: 4px;
}
/* Hide only for mouse users — never remove :focus-visible */
:focus:not(:focus-visible) { outline: none; }focusWCAG
Let keyboard users jump straight to the main content.
export function SkipLink() {
return (
<a href="#main"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
focus:z-50 focus:rounded focus:bg-white focus:px-4 focus:py-2">
Skip to content
</a>
);
}
// <main id="main"> is the targetWCAGkeyboard
css
Visually-hidden (sr-only) utility
Hide text visually but keep it available to screen readers.
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}WCAGutility
html
Lazy-load images with width/height
Prevent CLS by reserving space and deferring offscreen loads.
<!-- Reserve layout space: never let CLS jump -->
<img
src="hero.webp"
width="1280"
height="720"
loading="lazy"
decoding="async"
alt="Describe the image"
/>
CLSLCP
css
Avoid FOIT with font-display
Show fallback text immediately, then swap when the webfont is ready.
@font-face {
font-family: "Space Grotesk";
src: url("/fonts/space-grotesk.woff2") format("woff2");
font-display: swap;
font-weight: 400 700;
}fontsFOIT
html
Preload the LCP image
Hint the browser to fetch the largest element's image early.
<!-- In <head>: fetch the LCP image before CSS/JS parse -->
<link rel="preload" as="image" href="/hero.webp"
imagesrcset="/hero-480.webp 480w, /hero-1280.webp 1280w"
imagesizes="(max-width: 768px) 100vw, 1280px" />LCP
css
Tight, scalable spacing grid
A consistent spacing scale as CSS custom properties.
:root {
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
--space-16: 64px;
}
.card { padding: var(--space-6); gap: var(--space-3); }design systemtokens
css
Banding-free gradients with OKLCH
Perception-friendly color interpolation avoids muddy midpoints.
background: linear-gradient(
135deg,
oklch(0.7 0.15 40),
oklch(0.55 0.2 280)
);
/* color(srgb ...) fallback for older browsers before it */
@supports not (background: oklch(0 0 0)) {
.el { background: linear-gradient(135deg, #e0763f, #6a5bd0); }
}colorgradient
Make components respond to their container, not just the viewport.
.card-list { container-type: inline-size; }
.card { display: grid; gap: 1rem; }
@container (min-width: 480px) {
.card { grid-template-columns: 200px 1fr; }
}responsivecomponents
tsx
Stable callbacks with useCallback
Avoid re-render churn by memoizing handlers passed to children.
export function List() {
const [items, setItems] = useState<string[]>([]);
const addItem = useCallback((v: string) => {
setItems((prev) => [...prev, v]);
}, []);
return (
<ul>
{items.map((it, i) => (
<Row key={i} value={it} onRemove={removeItem(i)} />
))}
</ul>
);
}hooksmemo
tsx
Lift shared state with Context
Share theme/auth state without prop drilling.
const ThemeCtx = createContext<{
theme: "light" | "dark";
toggle: () => void;
} | null>(null);
export function useTheme() {
const ctx = useContext(ThemeCtx);
if (!ctx) throw new Error("useTheme must be inside <ThemeProvider>");
return ctx;
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light"|"dark">("dark");
const toggle = useCallback(() => setTheme(t => t === "dark" ? "light" : "dark"), []);
return (
<ThemeCtx.Provider value={{ theme, toggle }}>{children}</ThemeCtx.Provider>
);
}contextstate