Back to blog
Tutorial Apr 5, 2026 6 min read

Tutorial: zero-CLS hero sections in React

Step-by-step recipe to build cinematic heroes that never trigger layout shifts.

article.body

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.

Want this for your site?
Get in touch with our SEO experts.
Contact us