Ghost In The Code is a bespoke digital identity system built for a fictional high-immersion agency operating in the gaming and creative tech space. The brief was extreme: every interaction should feel like jacking into a cyberpunk HUD. The result is a fully componentized React application with custom particle systems, GLSL-inspired CSS effects, scan-line overlays, CRT sweep animations, and a data stream sidebar — all running at 60fps with zero external animation libraries.

No animation libraries. No shortcuts. Every effect is hand-rolled.
| Framework | React 19 + Vite Chosen for fast HMR during heavy CSS iteration and clean CSS Modules support. The component tree grew to ~46 files across atmosphere/, layout/, sections/, and ui/ — Vite's build performance made that scale manageable. |
| Architecture | Fully componentized + barrel exports Every section, atmospheric effect, and UI primitive lives in its own component with a co-located CSS Module. Six index.js barrel files keep imports clean across the tree. Config is centralized in siteConfig.js — all copy, data arrays, and site-level constants live there. |
| Animation | CSS Modules + Canvas API The constraint was zero npm installs beyond React and React-DOM. All effects — glitch flash, CRT sweep, scan-line overlay, scroll reveals, HUD corner brackets — are pure CSS keyframe animations. The particle network is a requestAnimationFrame canvas loop managed in a custom useParticles hook. |
| Scroll | Intersection Observer A reusable useInView hook drives all scroll-triggered reveals via IntersectionObserver. No scroll event listeners, no layout thrash. Each Reveal component wraps children with configurable delay and threshold. |
The atmosphere had to be felt, not noticed. That means 60fps or nothing.
The site layers multiple concurrent effects: an animated particle network on canvas, a CSS scan-line overlay running at full viewport, a CRT sweep line animating continuously, a data stream sidebar ticking character-by-character, and scroll-triggered reveals firing across every section. The risk was compounding — each effect is cheap in isolation, but together they could easily saturate the main thread and produce jank on mid-range mobile hardware.
The particle network runs entirely off the main thread concern by keeping particle count capped and using canvas compositing rather than DOM nodes. The data stream sidebar uses a setInterval tick with a character pool — no React state churn, just a direct canvas write. Scan-lines and the CRT sweep are pure CSS — GPU-composited, zero JavaScript cost. Scroll reveals use IntersectionObserver, which fires on the browser's idle callback rather than blocking scroll events.
The result is a site that reads as heavily animated but keeps its main-thread budget lean. Lighthouse performance scores stayed above 90 on desktop throughout development.
// useParticles — self-contained canvas animation loop
export function useParticles(canvasRef, count = 80) {
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
let raf;
const particles = Array.from({ length: count }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
}));
const tick = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((p) => {
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
});
// draw edges between nearby particles
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [canvasRef, count]);
}Hindsight is the best code reviewer.
The particle network was built directly into the component early in development, which made it harder to tune when I needed to adjust particle count and connection threshold independently per viewport size. I extracted it into a hook eventually, but I'd reach for that abstraction from day one next time — a useParticles hook that accepts a config object and handles resize via ResizeObserver would have been cleaner and reusable across the data stream and canvas effects.
I'd also invest earlier in a proper design token system. The palette — electric yellow, red, teal — is consistent throughout, but the glow values and opacity levels for hover states are hand-tuned per component rather than derived from a shared scale. A token file with named glow intensities (--glow-sm, --glow-md, --glow-lg) would make global atmosphere adjustments a one-line change rather than a grep-and-replace across 46 CSS Modules.