Skip to content

Deep Dive Into Responsive Images in Next.js

Delivering an optimal image experience tailored to each user’s device and screen size is crucial. Responsive images are a key technique ensuring images load quickly, maintain crisp quality, and adapt seamlessly across various viewports. This blog post explores what responsive images are, how to implement them in a framework such as Next.js, and how Cloudinary’s CldImage component built for Next.js simplifies the process.

Responsive images enable the delivery of high-quality images that adapt to the user’s device characteristics, such as screen size, resolution, and network conditions. Instead of serving a single, large image to all devices, responsive images provide different versions of the same image tailored to each device’s capabilities. This approach ensures that users on mobile devices or slower networks receive optimized, smaller images, thus improving performance and reducing data usage. Even pixel density is considered; devices with retina displays, like modern MacBooks, can receive images of higher pixel density if supported by the website.

HTML provides basic tools to facilitate responsive images through the srcset and sizes attributes on the img element. They can be used on the img element as on the following example:

<img
  srcset="
    image-320w.jpg 320w,
    image-480w.jpg 480w,
    image-800w.jpg 800w
  "
  sizes="(max-width: 320px) 280px, (max-width: 480px) 440px, 800px"
  src="image-800w.jpg"
  alt="Responsive Image"
/>
Code language: HTML, XML (xml)

The srcset attribute contains the image’s different variations, where each variation is separated by a comma and provides the image’s filename and the image’s intrinsic size. In the above example, image-320w.jpg has a size of 320w, meaning it has an intrinsic width of 320 pixels. Alternatively, we could also use the image’s pixel density descriptor instead of the size (e.g., 2x) if we wanted to provide images of different pixel densities (e.g., standard and retina, which has more pixels).

The sizes property, on the other hand, is a comma-separated list of media conditions and slot sizes that are used when the media condition is evaluated to be true. The last value in the list is sometimes without any media conditions, meaning it is the fallback slot size when no media condition is matched. But how does the browser use this list?

With srcset and sizes in place, the browser will look at characteristics of the user’s device such as screen size and pixel density, find the first matching media query that matches the user’s device, look at the slot size given to that media query, and load the image referenced in the srcset list that has the same size as the slot or, if there isn’t one, the first image that is bigger than the chosen slot size.

Now, with the above example in mind, this means that:

  • For devices up to 320px wide, the browser will pick the first image from the srcset that is equal to or larger than 280px, and that is image-320w.jpg.
  • For devices up to 480px wide, the browser will pick the first image from the srcset that is equal to or larger than 440px, and that is image-480w.jpg.
  • For devices larger than 480px wide, the browser will pick the first image from the srcset that is equal to or larger than 800px (the fallback value), and that is image-800w.jpg.

How is src used when srcset exists? It depends on whether the browser supports srcset. If the browser doesn’t support srcset, or if there’s an error parsing the srcset attribute, it will fall back to using the image source specified in the src attribute.

One relatively newer feature worth mentioning is Client Hints. While an image’s srcset is used on the client side by the browser to choose between different image resources based on device capabilities, Client Hints complement this by allowing the server to make more informed decisions about which images to serve based on additional information about the client’s device, leading to potentially even more optimized content delivery. This is accomplished through the browser sending special headers to the server that contain information about the client device capabilities, such as Sec-CH-DPR (which represents the browser window’s device pixel ratio). Client Hints still have limited browser support, however, the Cloudinary API already supports them.

Layout shift in the context of an image loading occurs when the browser initially renders a web page without knowing the actual dimensions of an image. As a result, the image’s placeholder takes up minimal space on the page, causing other elements to position themselves accordingly. However, once the image has fully loaded and the browser recognizes its actual dimensions, it may need to reflow the content, shifting surrounding elements to accommodate the image’s true size.

This effect can be seen below:

CleanShot 2024-03-28 at 20.31.41

To prevent this layout shift before an image has loaded, it’s recommended to provide the width and height attributes on the img element. By specifying these attributes, the browser can reserve the appropriate amount of space for the image based on its aspect ratio, preventing surrounding content from shifting when the image loads. This technique is known as “reserving space” and helps maintain a stable layout, improving the overall user experience. Therefore, to provide a good user experience for responsive images, we should not only rely on srcset and sizes, but provide width and height as well.

It’s important to note that while the width and height attributes define the intrinsic dimensions, the actual rendered size of the image can be different based on the applied CSS styles, such as max-width or object-fit.

If your website features a multitude of images, manually adding different variations in srcset can be a daunting task. Fortunately, there are tools like the Responsive Image Breakpoints Generator that help you alleviate this issue. Furthermore, modern frameworks such as Next.js come equipped with built-in functionality to generate a set of responsive images with no additional effort required by the developer. All that is required from you is to provide the image source, and Next.js will leverage its Image Optimization API to generate a set of images along with the accompanying srcset. This is achieved through the use of an Image Loader, essentially a function that creates multiple URLs to request the image at different sizes. These multiple URLs facilitate the automatic generation of srcset, ensuring that visitors to your site are served an image that is the perfect size for their viewport.

The default loader for Next.js applications is capable of optimizing images not only hosted locally but also those from anywhere on the web, and then serving them directly from the Next.js web server.

The Next.js Image component acts as a wrapper around the standard img element. By utilizing the Image component in the following manner, you can take advantage of Next.js’s image optimization capabilities. For an Image component used like this:

import Image from 'next/image'

const MyComponent = () => (
  <Image
    src="/assets/nature-mountains.jpg"
    width={800}
    height={600}
    alt="Example Image"
  />
)
Code language: JavaScript (javascript)

Next.js will generate an img element like this:

<img alt="Example Image" loading="lazy" width="800" height="600" decoding="async" data-nimg="1" style="color:transparent" srcset="/_next/image?url=%2Fassets%2Fnature-mountains.jpg&amp;w=828&amp;q=75 1x,/_next/image?url=%2Fassets%2Fnature-mountains.jpg&amp;w=1920&amp;q=75 2x" src="/_next/image?url=%2Fassets%2Fnature-mountains.jpg&amp;w=1920&amp;q=75">
Code language: HTML, XML (xml)

If Next.js’s Image component is a wrapper around the HTML img element, then Cloudinary’s CldImage is a wrapper around Next.js’s Image. The CldImage component accepts similar props as the Image component from Next.js but automatically handles image optimization, responsive delivery, and various image transformations through Cloudinary’s powerful API. This gives you access to advanced features like dynamic cropping, background removal, overlays, and other Cloudinary transformations.

image-20240328184014372

CldImage can be used almost as a drop-in replacement for the Next.js image component, as shown below. We only changed the element name from Image to CldImage and updated the source to match the location of the image on Cloudinary.

import { CldImage } from "next-cloudinary";

const MyComponent = () => (
  <CldImage
    src="samples/landscapes/nature-mountains"
    width={800}
    height={600}
    alt="Example Image"
  />
)
Code language: JavaScript (javascript)

The resulting img element is this:

<img alt="Example Image" loading="lazy" width="800" height="600" decoding="async" data-nimg="1" style="color:transparent" srcset="https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_828/f_auto/q_auto/v1/samples/landscapes/nature-mountains?_a=BAVAEyBy0 1x, https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_1920/f_auto/q_auto/v1/samples/landscapes/nature-mountains?_a=BAVAEyBy0 2x" src="https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_1920/f_auto/q_auto/v1/samples/landscapes/nature-mountains?_a=BAVAEyBy0">
Code language: HTML, XML (xml)

As you can see, the srcset now uses Cloudinary URLs for each image in the srcset. In the following sections, we will see how we can utilize CldImage in some common UI patterns.

Responsive images can be used in various scenarios across web applications, but they are particularly prevalent in hero sections and card elements, where visual appeal and optimal presentation are crucial. In this context, we will demonstrate how to model these components effectively by leveraging the powerful combination of React, Tailwind CSS, and Cloudinary’s CldImage component.

A hero section on a website typically refers to the prominent, eye-catching area at the top of a web page. The hero section often occupies a significant portion of the initial viewport and can include various elements. Commonly, it features a large image in the background that stretches to fill the entire width.

To achieve this, we will use CldImage with the sizes property set to 100vw. This means the image will always be full-width, allowing the browser to calculate the width of the rendered image depending on the user’s device and select the appropriate image from the auto-generated srcset based on that.

In a hero section like this, layout shift isn’t a concern because the image is used as a background image, and we don’t need to specify the width and height of CldImage. Instead, we need to use fill, which causes the image to fill the parent element. This is the resulting code:

function ResponsiveHero() {
  return (
    <div className="relative">
      <div className="absolute inset-0">
        <CldImage
          fill
          src="samples/landscapes/nature-mountains"
          sizes="100vw"
          alt="Lake with mountains in the background, blue sky and green trees along the shore"
        />
        <div className="absolute inset-0 bg-gray-800 opacity-50"></div>
      </div>
      <div className="relative max-w-7xl mx-auto py-24 px-4 sm:py-32 sm:px-6 lg:px-8">
        <h1 className="text-4xl font-extrabold tracking-tight text-white sm:text-5xl lg:text-6xl">
          Conquer Towering Peaks: Your Ultimate Mountain Adventure Awaits
        </h1>
        <p className="mt-6 text-xl text-gray-300">
          Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem
          cupidatat commodo. Elit sunt amet fugiat veniam occaecat fugiat
          aliqua. Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure
          qui lorem cupidatat commodo.
        </p>
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

When rendered, the hero section will appear as:

CleanShot 2024-03-20 at 22.36.17@2x

The result of the above code is that CldImage generated a total of eight srcset images (part of the URL is omitted for simplicity).

1. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_640/[...]/nature-mountains?_a=BAVAEyBy0 640w
2. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_750/[...]/nature-mountains?_a=BAVAEyBy0 750w
3. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_828/[...]/nature-mountains?_a=BAVAEyBy0 828w
4. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_1080/[...]/nature-mountains?_a=BAVAEyBy0 1080w
5. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_1200/[...]/nature-mountains?_a=BAVAEyBy0 1200w
6. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_1920/[...]nature-mountains?_a=BAVAEyBy0 1920w
7. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_2048/[...]/nature-mountains?_a=BAVAEyBy0 2048w
8. https://res.cloudinary.com/daa2k5bgw/image/upload/c_limit,w_3840/[...]/nature-mountains?_a=BAVAEyBy0 3840w

As you can see from the URLs, they contain parameters that determine the size and quality of the image you would receive if you made a GET request. The parameter starting with w_ specifically indicates the width of the image. This means that you, as a developer, no longer need to manually generate the images — Cloudinary, with its CldImage, will do all the heavy lifting for you.

Now, for someone using a 1080p monitor, the width of the viewport will be roughly 1920 pixels, and the browser will choose picture number 6 from the srcset, which is an image optimally sized for that resolution.

Card components are a popular UI pattern used in modern web design, often featuring a visually appealing layout with images, text, and other content elements. Images play a crucial role in card components, frequently appearing on the left or right side, serving as an eye-catching thumbnail or visual representation of the card’s content. Because images no longer appear in the background, we need to take a slightly different approach. This time, we need to be wary of possible layout shifts while the image is loading.

For that reason, we need to reserve some space for the image by specifying both width and height. We’ll also specify multiple sizes, because we want the image to be full-width when on mobile, while on larger screens, it will appear in the left section and be roughly 640px in width.

The resulting code is:

function ResponsiveCard() {
  return (
    <div className="bg-white rounded-lg border-2 overflow-hidden grid grid-cols-1 sm:grid-cols-2 max-w-screen-lg mx-auto my-32">
      <div className="flex justify-center p-4">
        <CldImage
          className="w-full lg:w-auto object-cover"
          width={478}
          height={319}
          src="samples/landscapes/nature-mountains"
          sizes="(max-width: 640px) 100vw, 750px"
          alt="Lake with mountains in the background, blue sky and green trees along the shore"
        />
      </div>
      <div className="p-6">
        <h3 className="text-xl font-semibold mb-2">Conquer Towering Peaks: Your Ultimate Mountain Adventure Awaits</h3>
        <p className="text-gray-600">Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem
          cupidatat commodo. Elit sunt amet fugiat veniam occaecat fugiat
          aliqua. Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure
          qui lorem cupidatat commodo.</p>
      </div>
    </div>
  );
}
Code language: JavaScript (javascript)

When rendered, the card section will appear as:

CleanShot 2024-03-21 at 10.26.56@2x

Responsive images are crucial for delivering an exceptional user experience, especially in today’s multi-device landscape. While Next.js provides built-in tools for implementing responsive images, Cloudinary’s CldImage component offers a powerful and flexible solution for handling complex image optimization and transformation scenarios. By combining Next.js and Cloudinary, you can easily deliver high-quality, responsive images that enhance your application’s performance and user experience.

Back to top

Featured Post