Working with Images in Stylesheets with PostCSS

Avatar of Aleks Hudochenkov
Aleks Hudochenkov on (Updated on )

The following is a guest post by Aleks Hudochenkov. Aleks does a great job here of showcasing what PostCSS is good at and the role it has grown into in the front end stack. That is: doing little useful jobs within CSS. You’re about to see a variety of PostCSS plugins at work that are all related to working with images. By the end, I bet you’ll be able to imagine how PostCSS can be useful for other niches within working with CSS.

We all work with images in our CSS. Routine stuff. We may not even realize it, but there many be a lot of manual work involved this with that could be made a lot easier. I’m going to be showing you a variety of PostCSS plugins that are specfically designed to help with working with images in CSS.

Every plugin described in this article works with every syntax PostCSS can parse — CSS, SCSS, Less and syntaxes created by PostCSS plugins. I will not describe how to use PostCSS itself, because there is already an excellent article by Drew Minns.

Let’s start with plugins which cover most of the use cases when dealing with images in CSS.

Image Helpers

The postcss-assets plugin is an almost essential plugin for dealing with images. It has lots of functionality.

Inlining images

Sometimes making images into data URL’s inside our stylesheet is useful. One less HTTP request!

/* input.css */
div {
  background: inline("images/logos/postcss.png");
}

/* output.css */
div {
  background: url("data:image/png;base64,iVBORw0KGgoAAAA...ggg==");
}

Calculating dimensions

Sometimes you need to size elements or size the background itself based on the dimensions of the image you are using. This plugin can make those measurements and output them as needed.

/* input.css */
body {
  width: width("images/foobar.png");
  height: height("images/foobar.png");
  background-size: size("images/foobar.png");
}

/* output.css */
body {
  width: 320px;
  height: 240px;
  background-size: 320px 240px;
}

If we dealing with high-density images, we can correct the output by adding the second parameter:

/* input.css */
body {
  width: width("images/foobar.png", 2);
  height: height("images/foobar.png", 2);
  background-size: size("images/foobar.png", 2);
}

/* output.css */
body {
  width: 160px;
  height: 120px;
  background-size: 160px 120px;
}

URL resolution

The plugin can help with file paths. We do not need to know the full path to the image. Only the filename is enough.

For example, we have this folder structure:

images/
 logos/
  postcss.png
input.css

We set up the options for the plugin like this. The ** means search in all folders and files, recursively.

postcss([
  require('postcss-assets')({
   loadPaths: ['**']
  })
])
/* input.css */
div {
  background: resolve("postcss.png");
  background: resolve("logos/postcss.png");
}

/* output.css */
div {
  background: url("/images/logos/postcss.png");
  background: url("/images/logos/postcss.png");
}

Cachebusting

This plugin can cachebust images.

postcss([
  require('postcss-assets')({
    cachebuster: true
  })
])
/* input.css */
div {
  background: resolve("images/logos/postcss.png");
}

/* output.css */
div {
  background: url("images/logos/postcss.png?153bd5d59c8");
}

Inline and modify SVGs

Almost every graphic I deal with lately is SVG. It’s a great format that handles any pixel density display. Even better, the syntax for it is text, meaning we can edit them without heavy tools like graphic editing programs.

There is a plugin for inlining SVGs: postcss-inline-svg. You might ask why we need it when postcss-assets, which we already covered, can do it. The reason is postcss-inline-svg has a killer feature: it can modify SVG.

Say we have a star icon that we use in ten different places with different colors around our site. There are many ways we can do this. We could use an inline SVG system with <symbol></symbol> and <use> and all that. Or, we can use background property in CSS!

There are two ways to use an image in CSS. 1) a url(/path/to/image.jpg) value with a file path or 2) a url(data:...) value with a data URL. The later is sometimes known as “inlining” images, which accomplishes one of the major advantages of image sprites: combining HTTP requests. With postcss-inline-svg, we can do this (making our CSS file our sprite), and still adjust the colors independently:

/* input.css */
.star--red {
  background-image: svg-load("img/star.svg", fill=#f00);
}
.star--green {
  background-image: svg-load("img/star.svg", fill=#0f0, stroke=#abc);
}

/* output.css */
.star--red {
  background: url("data:image/svg+xml;charset=utf-8,%3Csvg fill='%23f00'%3E...%3C/svg%3E");
}
.star--green {
  background: url("data:image/svg+xml;charset=utf-8,%3Csvg fill='%230f0' stroke='%23abc'%3E...%3C/svg%3E");
}

Will the output CSS file be enormous, you ask? Yes and no. The output CSS will be bigger because of code duplication, but it doesn’t matter because of Gzip! To demonstrate, I’ve made a test. I’ve created a CSS file with 100 different selectors and add an inline icon in every ruleset with a random color for the fill. Like so:

.wibcsidpuaeqgbxvcjqq { 
  background: svg-load("images/star.svg", fill: #8c0); 
}

Then I created a duplicate of this file, and removed all those inline backgrounds. Here are the file size results:

Original size Gzipped
With 100 images 48500 bytes 2560 bytes
With 1 image 3158 bytes 1817 bytes

Difference: 2560 – 1817 = 743 bytes

Not a big difference!

The only con of this approach: there is no way to animate changes of image. For example, if color should change on hover with a transition, there is no convenient way to do this because transition will not be applied to background-image.

These plugins compliment each other

Real world example: we need a button that is an icon. The image inside the button needs a specific image size and also needs to change color on hover. There is only one source SVG file.

Markup:

<button type="button" class="delete">Delete</button>

Without any help, we might do something like this:

.delete {
  box-sizing: content-box;
  padding: 15px;

  /* Values based on this particular image */
  width: 26px;
  height: 32px;

  border: 1px solid #ef5350;
  border-radius: 3px;
  background: #fff url("images/trash.svg") 50% 50% no-repeat;
  text-indent: -9999px;
}
.delete:hover {
  border-color: #c62828;

  /* Manually duplicate file and change things */
  background-image: url("images/trash-hover.svg");
}

Automating things with postcss-assets, we could do this:

postcss([
  require('postcss-inline-svg')(),
  require('postcss-assets')()
]);
.delete {
  box-sizing: content-box;
  padding: 15px;
  width: width("images/trash.svg");
  height: height("images/trash.svg");
  border: 1px solid #ef5350;
  border-radius: 3px;
  background: #fff svg-load("images/trash.svg", fill=#ef5350) 50% 50% no-repeat;
  text-indent: -9999px;
}
.delete:hover {
  border-color: #c62828;
  background-image: svg-load("images/trash.svg", fill=#c62828);
}

Output:

.delete {
  box-sizing: content-box;
  padding: 15px;
  width: 26px;
  height: 32px;
  border: 1px solid #ef5350;
  border-radius: 3px;
  background: #fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='32' viewBox='12 8 26 32' fill='%23ef5350'%3E%3Cpath d='M20 18h2v16h-2z'/%3E%3Cpath d='M24 18h2v16h-2z'/%3E%3Cpath d='M28 18h2v16h-2z'/%3E%3Cpath d='M12 12h26v2H12z'/%3E%3Cpath d='M30 12h-2v-1c0-.6-.4-1-1-1h-4c-.6 0-1 .4-1 1v1h-2v-1c0-1.7 1.3-3 3-3h4c1.7 0 3 1.3 3 3v1z'/%3E%3Cpath d='M31 40H19c-1.6 0-3-1.3-3.2-2.9l-1.8-24 2-.2 1.8 24c0 .6.6 1.1 1.2 1.1h12c.6 0 1.1-.5 1.2-1.1l1.8-24 2 .2-1.8 24C34 38.7 32.6 40 31 40z'/%3E%3C/svg%3E") 50% 50% no-repeat;
  text-indent: -9999px;
}
.delete:hover {
  border-color: #c62828;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='32' viewBox='12 8 26 32' fill='%23c62828'%3E%3Cpath d='M20 18h2v16h-2z'/%3E%3Cpath d='M24 18h2v16h-2z'/%3E%3Cpath d='M28 18h2v16h-2z'/%3E%3Cpath d='M12 12h26v2H12z'/%3E%3Cpath d='M30 12h-2v-1c0-.6-.4-1-1-1h-4c-.6 0-1 .4-1 1v1h-2v-1c0-1.7 1.3-3 3-3h4c1.7 0 3 1.3 3 3v1z'/%3E%3Cpath d='M31 40H19c-1.6 0-3-1.3-3.2-2.9l-1.8-24 2-.2 1.8 24c0 .6.6 1.1 1.2 1.1h12c.6 0 1.1-.5 1.2-1.1l1.8-24 2 .2-1.8 24C34 38.7 32.6 40 31 40z'/%3E%3C/svg%3E");
}

If the image changes, you don’t need to do anything! postcss-assets will update sizes. Need to change a color? They are right here in CSS. You can even use variables if you are using another plugin or preprocessor that offers those.

Example of output:

See the Pen NNvGJP by Aleks Hudochenkov (@hudochenkov) on CodePen.

Sprites

There are still some reasons you might want to use the kind of image sprite where all the images are combined together into one larger image. For one, it is known that mobile phones decode inline images slightly slower than regular images.

There are lots of tools for creating image sprites. For example: grunt-spritesmith. These are powerful, aren’t particularly easy to set up or convenient. In grunt-spritesmith, for example, you need to understand how its templates engine works.

The postcss-sprites plugin (based on spritesmith) is much more convenient. This is how it works:

/* input.css */
.comment {
  background-image: url("images/sprite/ico-comment.png");
}
.bubble {
  background-image: url("images/sprite/ico-bubble.png");
}

/* output.css */
.comment {
  background-image: url("images/sprite.png");
  background-position: 0 0;
}
.bubble {
  background-image: url("images/sprite.png");
  background-position: 0 -50px;
}

It finds every image in CSS (filtering is possible), creates a sprite, and outputs the correct background-position to get there.

Handling sprites for high-density screens

Despite postcss-sprites support for retina images, it doesn’t quite give you production ready code. For example, it doesn’t give you media queries to target high-density screens to actually use those images. This problem can be solved with another PostCSS plugin. This is a beauty of PostCSS ecosystem — there are many plugins which do only one job, and you can combine them for solving more complicated problems.

There is a postcss-at2x plugin which adds media queries targeting high-density screens. Let’s combine these plugins to generate a sprite for both normal and high-density screens.

postcss([
  require('postcss-at2x')(),
  require('postcss-sprites').default({
    retina: true
  })
]);
/* input.css */
.circle {
  background-image: url("images/circle.png") at-2x;
}
.square {
  background-image: url("images/square.png") at-2x;
}

/* output.css */
.circle {
  background-image: url("sprite.png");
  background-position: 0px 0px;
}
.square {
  background-image: url("sprite.png");
  background-position: -25px 0px;
}
@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi), (min-resolution: 1.5dppx) {
  .circle {
    background-image: url("[email protected]");
    background-position: 0px 0px;
    background-size: 50px 25px;
  }
}
@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 144dpi), (min-resolution: 1.5dppx) {
  .square {
    background-image: url("[email protected]");
    background-position: -25px 0px;
    background-size: 50px 25px;
  }
}

Nailed it.

Create images on the fly

Sometimes we need really simple images (like geometric shapes), but still find ourselves opening a graphic editor, creating the image, exporting it, putting it in the right place, optimizing it, and using it in CSS. What if we can could create simple images right in a CSS? I bet you can guess: we can!

postcss-write-svg allows you to create simple SVG images right in CSS. Just describe SVG elements and it will be inlined as background-image.

/* input.css */
@svg square {
  @rect {
    fill: var(--color, black);
    width: 100%;
    height: 100%;
  }
  @polygon {
    fill: green;
    points: 50,100 0,0 0,100;
  }
}

#example {
  background: white svg(square param(--color #00b1ff));
}

/* output.css */
#example {
  background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2300b1ff' width='100%25' height='100%25'/%3E%3Cpolygon fill='green' points='50%2C100 0%2C0 0%2C100'/%3E%3C/svg%3E");
}

There are other plugins to make circles and triangles with only CSS properties. Triangles. You can do triangles yourself in CSS, but it’s not exactly straightforward and gets harder when you want to do different types of triangles. postcss-triangle allows to you to easily create isosceles, right isosceles and equilateral triangles.

/* input.css */
.isosceles-triangle {
  triangle: pointing-right;
  width: 150px;
  height: 115px;
  background-color: red;
}
.right-isosceles-triangle {
  triangle: right-iso pointing-down;
  width: 250px;
  background-color: red;
}
.equilateral-triangle {
  triangle: equilateral pointing-up;
  height: 100px;
  background-color: red;
}

/* output.css */
.isosceles-triangle {
  width: 0;
  height: 0;
  border-style: solid;
  border-color: transparent;
  border-width: 57.5px 0 57.5px 150px;
  border-left-color: red;
}
.right-isosceles-triangle {
  width: 0;
  height: 0;
  border-style: solid;
  border-color: transparent;
  border-width: 125px 125px 0;
  border-top-color: red;
}
.equilateral-triangle {
  width: 0;
  height: 0;
  border-style: solid;
  border-color: transparent;
  border-width: 0 57.73503px 100px;
  border-bottom-color: red;
}

Circles are a bit easier, but postcss-circle may save you a few lines of code and add more readability.

/* input.css */
.circle {
  circle: 100px red;
}

/* output.css */
.circle {
  border-radius: 50%;
  width: 100px;
  height: 100px;
  background-color: red;
}

Cachebusting

Say you need to update an image linked to in a stylesheet. We might run into a problem if we’re using far out expires headers, and the user’s browser is hanging onto that image in cache. The solution is to force that user’s browser to download the new version (cachebust). There are two ways to do this: change a filename or change the URL. Changing the filename is a lot to ask, but changing URL is easy thanks to URL parameters.

This is how changing URL works with postcss-urlrev:

/* input.css */
.foo {
  background: url("images/test.png") 0 0 no-repeat;
}

/* output.css */
.foo {
  background: url("images/test.png?v=e19ac7dee6") 0 0 no-repeat;
}

This task can be done also with postcss-cachebuster and postcss-assets.

Utilities

PostCSS plugins can help with optimizing stylesheets. For example postcss-svgo can optimize inlined SVG with SVGO, the best SVG optimization tool.

If you still need to support browsers which doesn’t support SVG, but you like to use SVG, postcss-svg-fallback can help you. This plugin generates PNG fallbacks for SVG in you CSS (both inlined and linked via url()) and adds additional rules in CSS for old browsers.

Inlining images might bloat output CSS, but there is a solution: postcss-data-packer can extract embedded data URLs into a separate file. Now you can load this file asynchronously to decrease page load time.

Conclusion

Before PostCSS, we did a lot of tedious manual work: copy and pasting things, duplicating things, or manual calculations. Now we can use some PostCSS plugins, to make our computers do things for us. It speeds up our work and makes us happier people.