Something very popular in web design currently is overlapping images. When the design is handed to you, as the developer to implement it, there are a few ways to go about it like most things with CSS.

One way you might approach it is you could absolutely position one element with a lower z-index to make the other image sit on top, adjust the widths on each image, so you can see both of them and call it day, right?

Wellllll, once you need text or any other content after the images, you’ll run into a problem. If the absolute-positioned element is taller than the static (top) image, the following content will overlap the images. This is due to the height of the absolute-positioned image which is not recognized since it’s out of the document flow, (a normal behavior for an absolute positioned element). To get around this, you might begin to try out arbitrary heights on the images and the component then becomes very fragile, limited, and hacky real fast.

The next thing you know, you’re found days later with no food or water drinking the last of your uncontrollable tears. It’s a dark place, I don’t recommend it.

An example of what I’m talking about is here:

See the Pen
by Bri Camp (@brianacamp)
on CodePen.

The good news: there is a much better way and do not attempt that first route unless you enjoy pain.

I will write out two solid approaches to overlapping images without having content overlap our wonderful image component I’ll affectionately call the “image stack”.

Method 1: CSS Grid

Before I start hearing grumbles about needing to support IE and how you can’t use CSS Grid, I say you can use CSS Grid and easily use a fall back for IE which I’ll show you how to do in the last section of this post.

One of my ding dang favorite things about CSS Grid (besides the fr unit, or the min-max property) is the ability to overlap images by varying z-indices without taking a thing out of the normal document flow!

First we’re going to analyze this component:

A few things to note:

  1. The component will work regardless of any height of either top or bottom image. (Something to take into account when clients could upload any image via a CMS)
  2. The top image will always be a little pushed down from the top and will align with the left edge of the container.


The HTML Structure


Something we need to take into account is how the widths of the columns of the grid need to be. As mentioned before, you can approach this several ways, but I’m going to stick to using 12 columns since 12 column grids are used often.

In order to do that, on the parent element that contains the elements, in our CSS we’ll write:

  .image-stack {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    position: relative;

grid-template-columns is a property that dictates how many columns the grid will have and the 1fr value is for the browser to calculate the available space. It comes in handy when you have column and row gaps. Hooray for browser calculations!

position: relative is imperative here: that’s what allows the z-index on the images to work as expected.

So now that we have our grid working, the next step is looking at the widths of the images:

To add our widths to each image based on the design, my mind works in percentages, so let’s start with the total width of the image component which is 844px, which equals 100%. The top image’s width is 521px. I divide 521px / 844px * 100 which equals 61.7%, round that up to 62% and you get a number that’s exactly in the middle of 7/12ths (58%) and 8/12 (66%) ha! Let’s go with 66%.

For our top image, we’ll write the following in CSS:

    .image-stack__item--top {
      grid-column: 1 / span 8;
      grid-row: 1; // must be on the same row as the other image
      padding-top: 20%; // this pushes the image down, and keeps it proportional as it resizes
      z-index: 1; // make this image render on top of the bottom

For our second image, we’ll calculate (645px / 844) * 100 = 76.4%. We’ll round down to 75% which perfectly goes into our 12 column grid. 9/12ths. So, to do that, we’ll make sure our bottom image spans 9 columns and start it at the 4th grid line and span the entire rest of the grid which is what -1 does. You can think of -1 as the end if that helps!

Our bottom image CSS will look like this:

  .image-stack__item--bottom {
    grid-column: 4 / -1;
    grid-row: 1; // make this image be on the same row

See the Pen
by Bri Camp (@brianacamp)
on CodePen.

Et voila! With CSS grid and very little code you can start overlapping all the things that includes text over images, text over text (oh my!), images over canvas. You name it, the web is your oyster! Wee!

Method 2: Float with Negative Margins

So in the case that you have to support IE. Then this section is for you.

This method does require the ole “take the the dang element out of the document flow” tactic and we’ll be doing it with floats!

My friend and manager Jake Fentress came up with this solution which I applied to the Glossier site since we needed to support IE11.

The good news is the structure doesn’t change a bit.

For the image-stack parent element we’ll add a clearfix since we’re floating its child elements and need the content to render below:

.image-stack::after {
  content: ' ';
  display: table;
  clear: both;

Then for the top image we’ll add:

.image-stack__item--top {
    float: left;
    width: 66%;
    margin-right: -100%;
    padding-top: 15%; // arbitrary
    position: relative;
    z-index: 1;

The negative margin here is imperative for this to work. Negative margins are REALLY weird. Depending if it’s a negative margin on the top or bottom, it’ll behave differently if you apply a negative margin to the left and right and they work differently on floated elements.

We’re applying a negative right margin on a floated left element that allows content to overlap. -100% is equal to the width of the container so it shifts the image to the left and allows the bottom image to render beneath it as if it’s not in the DOM.

Codepen here:

See the Pen
overlapping images with float
by Bri Camp (@brianacamp)
on CodePen.

If you want more information over negative margins, I encourage you to read The Definitive Guide to Using Negative Margins. That article gives a deep dive into all of negative margin’s use cases and unexpected behaviors.

Fallback Solution (CSS Grid & Float Method)

This section will give you the best of both worlds, modern CSS for modern browsers and a fallback for IE11.

The most important part here is the @supports feature query. We’ll be using it to check if the browser supports the value of display: grid. We first put the IE or fallback CSS before we add the @supports feature query. Once we add the modern CSS, the floats will no longer be affected since grid is taking over. In the supports feature query, we’ll reset the width to 100% and you can either remove the float property or leave it be since it has no affect on the elements.

Here is the final codepen and example showcasing just that:

See the Pen
Overlapping images with css grid and fallback
by Bri Camp (@brianacamp)
on CodePen.

I hope this article was helpful for you and accomplishes your layout dreams! Go forth and overlap all the things!

Be well!


For every website, page load time is a critical factor that can make or break the business. Thanks to the better user experience that comes with a fast-loading webpage, those who focus on page load optimization enjoy better conversion rates, better SEO, better retention, and lower bounce rates.

And this fact has been highlighted in several studies. For example, according to one of the studies, 47% of consumers expect a web page to load in 2 seconds or less. No wonder that developers across the globe focus a lot on improving the webpage load time.

Logic dictates that keeping other factors the same, a lighter webpage should load faster than a heavier webpage, and that is the direction in which our webpages should head too. However, contrary to that logic, our websites have become heavier over the years. A look at data from HTTP Archive reveals that an average webpage in 2017 was almost three times heavier than what it used to be in 2011.

With more powerful user devices, faster networks, and the growing popularity of client-side frameworks and media-rich experiences, we have started pushing more and more data to the user’s device.

However, as developers, we miss a crucial point. We usually develop and test our websites in our offices over stable WiFi or wired connections. However, when a real-user uses our website, the network speed and stability may not be that great. Especially with more and more users coming online via mobile devices, the problem of fluctuating network conditions is even more significant.

Don’t believe it? conducted a study to determine the network speed reported by the Network Info API of Chrome browser for users of a website (with visitors mostly from India). It is not very surprising that almost 40% of the visitors tracked had reported speed lower than 4G, i.e., less than 700 Kbps as per the Network Info API Spec.

While this percentage of users experiencing poor network conditions might be lower if we get visitors from developed countries like the USA or those in Europe, we can still safely assume that varying network conditions impact a sizeable chunk of our users. And we have all experienced it as well. When we enter an elevator or move to the basement parking lot of a building or travel to a remote location, the mobile data download speeds drop significantly. 

Therefore, we need to keep in mind that our users, especially the ones on mobile, will invariably try to visit our website on a slow network, and our goal as a developer should be to provide them with at least a decent browsing experience.

Why optimize images for slow networks?

The ultimate goal of optimizing a website for slower networks is to be able to serve its lighter variant. This way, all the essential stuff gets downloaded and displayed quickly on the user’s device. 

Amongst all the resources that load on a webpage, images make up for most of the payload. And even if we do take care of optimizing the images in general, optimizing them further for slower networks can reduce the overall page weight by almost 30%. 

Also, additional compression of images doesn’t break the critical functionality of any application. Yes, the image quality drops a bit to provide for better user experience. But unlike stripping away Javascript, which would require a lot of thought, compressing images is relatively straightforward.

How to optimize images for a slow network?

Now that we have established that optimizing our webpage based on the user’s network speed is essential and that images are the lowest-hanging fruit to get started, let’s look at how we can achieve network-based image optimization.

There are two parts of the solution.

Determine the user’s network speed

We need to determine the network speed that the user is experiencing and divide them into buckets. For example, users experiencing speed above a certain threshold should be classified in a single group and served a particular quality level of an image. This classification is simple in modern web browsers with the Network Information API. This API automatically classifies the users into four buckets – 4G, 3G, 2G, and slow 2G, with 4G being the fastest and slow 2G being the slowest. 

// returns '4g', '3g', '2g' or 'slow-2g'
var effectiveType = NetworkInformation.effectiveType;

Compress the images to an appropriate quality level

The second part of the solution is to be able to alter the compression level of an image in real-time, depending on the user’s network speed determined in step 1. It should be as simple as passing an additional parameter in the image URL when the browser triggers a load for it.

While we rely on the browser to determine the user’s network speed, a tool like makes the real-time compression bit simple. is a real-time image optimization and transformation product that helps us deliver images in the right format, change compression levels, resize, crop, and transform images directly from the URL and deliver those images via a global image CDN. We can get the image with the desired compression level by just passing the image quality parameter in the URL. Quality is directly proportional to image size, i.e., higher the quality number, larger will be the resulting image.

// ImageKit URL with quality 90

// ImageKit URL with quality 50

How else does ImageKit help with network-based image optimization?

While ImageKit has always supported real-time URL-based image quality modification, it has started supporting the network-based image optimization features recently. With these new features, it has become effortless to implement complete network-based optimization with minimum effort.

Of course, first, we need to sign up for ImageKit and start delivering the images on our website through it. Once this is done, in the ImageKit dashboard, we have to enable the setting for network-based image optimization. We get a code snippet right there that and add it to an existing service worker on our website or to a new service worker. 

// Adding the code snippet in a service worker
importScripts(""   new Date().getTime());

Within the dashboard itself, we also need to specify the desired quality level for different network speed buckets. For example, we have used a quality of 90 for images for users classified as 4G users and a quality of 50 for our slow 2G users. Remember that lower quality results in smaller image sizes.

This code snippet is like a plugin meant for use in service workers. It intercepts the image requests originating from the user’s browser, detects the user’s network speed, and adds the necessary parameters to the image URL. These parameters can be understood by the ImageKit server to compress the image to the desired level and maintain efficient client-side caching. For network-based image optimization, all that we need to do is to include it on our website and done!

Additionally, in the ImageKit dashboard, we can specify the image URLs (or patterns in URLs) that should not be optimized based on the network type. For example, we would want to present the same crisp logo of our brand to our users regardless of their network speed.

Verifying the setup

Once set up correctly, we can quickly check if the network-based optimization is working using Chrome Developer Tools. We can emulate a slow network on our browser using the developer tools. If set up correctly, the service worker should add some parameters to the original image request indicating the current network speed. These parameters are understood by ImageKit’s servers to compress the image as per the quality settings specified in our ImageKit dashboard corresponding to that network speed.

How does caching work with the service worker in place?

ImageKit’s service worker plugin by-passes the browser cache in favor of network-based image cache in the browser. By-passing the browser cache means that the service worker can maintain different caches for different network types and choose the correct image from the cache or request a new one based on the user’s current network condition. 

The service worker plugin automatically uses the cache-first technique to load the images and also implements a waterfall method to pick the right one from the cache. With this waterfall method, images at higher quality get preference over images at a lower quality. What it means is that, if the user’s speed drops to 2G and he has a particular image cached from the time when he was experiencing good download speed on a 4G network, the service worker will use that cached 4G image for delivery instead of downloading the image over the 2G network. But the reverse is not valid. If the user is experiencing 4G network speeds, the service worker won’t pick up the 2G image from the cache, because it is possible to fetch a better quality image and the resources allow for it.

And there is more!

Apart from a simple, ready-to-use service worker plugin and dashboard settings, ImageKit has a few more things to offer that make it an attractive tool to get started with network-based optimization.


ImageKit provides us with analytics on our user’s observed network type. It gives us an insight into the number of times network-based optimization gets triggered and what does the user distribution look like across different network types. This distribution analysis can be helpful even for optimizing other resources on our website.

Cost of optimization

With network-based image optimizations, the size of the images goes down, but at the same time, the number of transformation requests can potentially go up. Unlike a lot of other real-time image optimization products out there, ImageKit’s pricing is based only on the output image bandwidth and nothing else. So with the favorable pricing model, implementing network-based image optimization not only provides a lot more value to our users but also helps us cut down on image delivery costs.


Improving page load performance is essential. However, there is one bit that we have all been missing – optimizing it for slow networks.

Images present an easy start when it comes to optimizing our entire website for different networks. With the support of network-based optimization features via service workers, it has become effortless to achieve it with ImageKit. 

It will be a great value add for our users and will help to improve the user experience even further, which will have a positive impact on the conversions on our website. 

Sign up now for ImageKit and get started with it now!


I recently found a solution to dynamically update the color of any product image. So with just one of a product, we can colorize it in different ways to show different color options. We don’t even need any fancy SVG or CSS to get it done!

We’ll be using an image editor (e.g. Photoshop or Sketch) and the image transformation service imgix. (This isn’t a sponsored post and there is no affiliation here — it’s just a technique I want to share.)

See the Pen

Dynamic Car color
by Der Dooley (@ddools)

on CodePen.

I work with a travel software company called CarTrawler on the engineering team, and I recently undertook a project a revamp our car images library that we use to display car rental search results. I wanted to take this opportunity to introduce dynamically colored cars.

We may sometimes load up to 200 different cars at the same time, so speed and performance are key requirements. We also have five different products throughout unique code bases, so avoiding over-engineering is vital to success.

I wanted to be able to dynamically change the color of each of these cars without needing additional front-end changes to the code.

Step 1: The Base Layer

I’m using car photos here, but this technique could be applied to any product. First we need a base layer. This is the default layer we would display without any color and it should look good on its own.

Step 2: The Paint Layer

Next we create a paint layer that is the same dimensions as the base layer, but only contains the areas where the colors should change dynamically.

A light color is key for the paint layer. Using white or a light shade of gray gives us a great advantage because we are ultimately “blending” this image with color. Anything darker or in a different hue would make it hard to mix this base color with other colors.

Step 3: Using the imgix API

This is where things get interesting. I’m going to leverage multiple parameters from the imgix API. Let’s apply a black to our paint layer.

(Source URL)

We changed the color by applying a standard black hex value of #000000.

If you noticed the URL of the image above, you might be wondering: What the heck are all those parameters? The imgix API docs have a lot of great information, so no need to go into greater detail here. But I will explain the parameters I used.

  • w. The width I want the image to be
  • bri. Adjusts the brightness level
  • con. Adjusts the amount of contrast
  • monochrome. The dynamic hex color

Because we are going to stack our layers via imgix we will need to encode our paint layer. That means replacing some of the characters in the URL with encoded values — like we’d do if we were using inline SVG as a background image in CSS.

Step 4: Stack the Layers

Now we are going to use imgix’s watermark parameter to stack the paint layer on top of our base layer.,middle&mark=[PAINTLAYER]

Let’s look at the parameters being used:

  • w. This is the image width and it must be identical for both layers.
  • mark-align. This centers the paint layer on top of the base layer.
  • mark. This is where the encoded paint layer goes.

In the end, you will get a single URL that will look like something like this:,middle&mark=

That gives the car in black:

(Source URL)

Now that we have one URL, we can basically swap out the black hex value with any other colors we want. Let’s try blue!

(Source URL)

Or green!

(Source URL)

Why not red?

(Source URL)

That’s it! There are certainly other ways to accomplish the same thing, but this seems so straightforward that it’s worth sharing. There was no need code a bunch of additional functionality. No complex libraries to manage or wrangle. All we need is a couple of images that an online tool will stack and blend for us. Seems like a pretty reasonable solution!


Table Of Contents

Unoptimized (non-minified) images are one of the main causes of poor website performance, mainly on the initial (first) load. Depending on the resolution and image quality, you might end up with images that take up more than 70% of your total website size.

It’s very easy for unoptimized images to end up on a production site and slow down its initial load considerably. Inexperienced devs usually aren’t aware of this potential problem. They also aren’t aware of a wide range of tools and approaches for optimizing images.

This article aims to cover most of the tools and approaches for optimizing images for the web.

Calculating JPG image file size

The uncompressed image size can be easily calculated by multiplying image width px value with image height px value and multiply the result by 3 bytes which is equivalent to 24 bits (RGB color system). We divide the result by 1,048,576 (1024 * 1024) to convert the value from bytes to megabytes.

image_size = (image_width * image_height * 3) / 1048576

For example, let’s calculate file size for an uncompressed image that has 1366px x 768px dimensions.

1366 * 768 * 3 / 1048576 = 3Mb

Considering that average website size today is between 2Mb and 3Mb, imagine having an image on your site that takes more than 80% the size of your site. 3Mb takes ages to load on slower mobile networks, so you might lose some traffic on your website if the user is waiting for your website to load and most time is spent on loading a single image. Scary thought, isn’t it?

So what we can do to avoid having optimized images on the web but preserve the acceptable quality and resolution?

Online image optimization

If you are working on a simple static website that only has a handful of images that won’t change often or won’t change at all, you can just drag and drop your images in one of the numerous online tools. They do an amazing job at compressing images using various algorithms and are more than enough for simple projects.

Alt Text

Most notable websites, in my opinion, are:

Automated solutions

However, if you are working on more complex projects with multiple people and using a lot of images, optimizing each one as it is added to the project can become tedious. Also, there is a risk that some images may end up not optimized due to human error or some other factor.

On complex projects, it’s common to use an equally complex build system like Gulp, Webpack, Parcel, etc. Image optimization plugins can be easily added to those build configs and fully automate the image optimization process. Images can be optimized as soon as they are added to the project.

Most notable plugin, in my opinion, is imagemin which can be easily integrated with any CLI or build tools:

Image loading optimization

We’ve looked at the image optimization strategies that reduce the file size by compressing the image without changing the image resolution and affecting image quality too much. Although optimizing image file reduces the file size of images considerably, having multiple optimized images (on the webshop catalog page for example) loaded all at once can have a poor effect on performance.

Lazy Loading

Lazy loading is a concept of only loading assets that are needed. In our case, only images that are currently within the user’s viewport (screen) are loaded. Other images are not loaded until they appear within the user’s viewport.

Although native Lazy loading has just been recently introduced to browsers, there have been many JavaScript-based solutions available.

Alt Text

Native Lazy Loading

 src="image.jpg" loading="lazy" alt="Sample image" />

JavaScript-based solutions

Most notable JavaScript-based solutions, in my opinion, are:

Progressive images

Although lazy loading does a great job performance-wise, looking at the problem from UX perspective we can see that the user is waiting for the image to load and looking at the blank space. On slow connections, downloading images can take ages. This is where progressive images come into play.

Basically, having a progressive image means that a low-quality image will be displayed to the user until a high-quality image has finished loading. A low-quality image has a considerably smaller file size due to the low quality and high compression rate, so this image will be loaded very fast. In between the low quality and high-quality image we can have as many images with varying quality as we need and we can load the higher quality image on each download.

Alt Text

Similarly to the article on Skeleton loading I’ve written, this technique gives the user an illusion of speed. User is looking at an image that is loading and becoming more clearer as it loads higher and higher quality image, instead of looking at the empty space waiting for something to happen.

This is an JavaScript implementation of progressive images: progressive-image

Responsive images

We also need to be careful of using properly-sized images.

For example, let’s say we have an image that is 1920px maximum width on desktop , 1024px maximum width on tablet devices and 568px maximum width on mobile devices. Simplest solution would be to just use the 1920px image and cover all the cases, right? In that case, an user on a smartphone with slow and unreliable connection would have to wait ages for the massive image to download and we’d be back at the square one of the problem.

Luckily for us, we can use picture element to tell the browser which image to dowload, depending on the media query. Although this element is supported by more than 93% of globally used browsers, it has a pretty simple fallback with img element already inside it.

   media="(min-width: 1025px)" srcset="image_desktop.jpg">
   media="(min-width: 769px)" srcset="image_tablet.jpg">
   src="image_mobile.jpg" alt="Sample image">

Using CDN

CDN services like Cloudinary and Cloudflare can perform image optimization on the server and serve the optimized images to the user. If your website uses a CDN, it’s worth looking into asset optimization options. This allows us not to worry about image quality optimization at all, and have all optimizations done server-side. We only need to look into optimizing image loading by either using lazy loading or progressive images.

WebP image format

WebP image format is developed by Google and is an image format specifically optimized for the web. According to the canIUse data, current browser support for WebP image format is at around 80% which is great. Luckily, implementing a fallback to standard jpg image with img element inside picture element is easy.

   type="image/webp" srcset="image.webp" />
   srcset="image.jpg" />
   src="image.jpg" alt="Sample image" />

Although there are numerous online file format converters that can convert images to WebP format, CDN services can easily perform format conversion server-side.

Optimization for high pixel density screens

This is more UX improvement rather than performance, but it’s also important to take into account devices that have higher pixel density.

For example, let’s assume that we are displaying an 768px x 320px banner image on 768px screen. But the screen has 2x density and the px width is actally: 2 x 768 = 1536px. Basically, we are stretching 768px over 1536px and this leads to blurry image on high pixel density devices.

In order to fix that, we need to serve an image optimized for high pixel density screens. We need to create separate images that are 2 times or 3 times the resolution of regular screens and use the srcset attribute with 2x tag marking for higher resolution image.

 src="image-1x.jpg" srcset="image-2x.jpg 2x" alt="Sample image" />

Example – Responsive WebP/PNG images with high-density screen support

     srcset="./images/webp/hero-image-420-min.webp 1x, ./images/webp/hero-image-760-min.webp 2x" type="image/webp" media="(max-width: 440px)">
     srcset="./images/minified/hero-image-420-min.png 1x, ./images/minified/hero-image-760-min.png 2x" media="(max-width: 440px)">
     srcset="./images/webp/hero-image-550-min.webp 1x, ./images/webp/hero-image-960-min.webp 2x" type="image/webp" media="(max-width: 767px)">
     srcset="./images/minified/hero-image-550-min.png 1x, ./images/minified/hero-image-960-min.png 2x" media="(max-width: 767px)">
     srcset="./images/webp/hero-image-420-min.webp 1x, ./images/webp/hero-image-760-min.webp 2x" type="image/webp" media="(max-width: 1023px)">
     srcset="./images/minified/hero-image-420-min.png 1x, ./images/minified/hero-image-760-min.png 2x" media="(max-width: 1023px)">
     srcset="./images/webp/hero-image-760-min.webp 1x, ./images/webp/hero-image-960-min.webp 2x" type="image/webp" media="(max-width: 1919px)">
     srcset="./images/minified/hero-image-760-min.png 1x, ./images/minified/hero-image-960-min.png 2x" media="(max-width: 1919px)">
     srcset="./images/webp/hero-image-960-min.webp" type="image/webp">
      src="./images/minified/hero-image-960-min.png" alt="Example">

Conclusion – Optimization priority

  1. Use optimized images (optimized by automated build tools, online services or CDN)
  2. Use lazy loading (JS solution until native becomes more supported)
  3. Optimize images for high pixel density screens
  4. Use WebP image format
  5. Use progressive images

Optional: Remember to serve images (and other static assets) over CDN if you are able to.

Alt Text

Thank you for taking the time to read this post. If you’ve found this useful, please give it a ❤️ or ?, share and comment.