The CLS trap in React heroes
Most React hero sections suffer from a brutal CLS spike between hydration and the moment the hero image loads. The skeleton has one height, the loaded image has another, and the page jumps. We're going to build a hero that never moves, even on a 3G connection.
Step 1: reserve space with aspect-ratio
Wrap the hero in a div with an explicit aspect-ratio CSS property. This reserves the exact final height before any image loads. Use aspect-ratio: 16 / 9 for cinematic, aspect-ratio: 4 / 5 for portrait commerce heroes. The browser computes the height from the container width — no layout thrash.
Step 2: preload the LCP image
Add a link rel=preload as=image with fetchpriority=high in the document head. This tells the browser to start downloading the hero image before the React bundle even parses. LCP drops by 200-400ms on cold loads.
Step 3: progressive enhancement with a blurhash
Render a low-quality placeholder (BlurHash or thumbhash) as a CSS background while the real image loads. Because the container has reserved space, the placeholder fades into the real image with zero shift. The page feels instant and never jumps.
Step 4: animate without shifting
Use CSS transforms (translate, scale, opacity) for hero animations — never width, height or top/left. Transforms run on the compositor and don't trigger layout, which means they don't trigger CLS. Framer Motion's animate prop on transform-based properties is safe; animating layout is not.
The final recipe
Aspect-ratio container + preload + blurhash + transform-only animations = a hero that loads cinematically and scores 0.00 CLS. Ship this pattern in every project. Your users — and your Core Web Vitals dashboard — will thank you.