Skip to content

Recipe: concise CSS templates for sprites

Eugene Lazutkin edited this page Mar 18, 2014 · 3 revisions

While grunt-tight-sprite is flexible enough to accommodate practically any CSS structure (or your favorite CSS preprocessor), it makes sense to plan ahead how to use sprites, especially for new projects unencumbered with legacy. Usually it makes sense to split sprite-related CSS into two parts:

  • static, common for all sprites.
  • dynamic, specific for different images. This part is generated by grunt-tight-sprite.

Avoid: combo CSS

Let's start with a plain vanilla CSS you can frequently find on Internet. It looks like that:

.sprite1 {
  background: url(sprite.png) -100px -100px no-repeat;
  width:  32px;
  height: 32px;
  /* other junk: position or float, padding, margin, font, and so on */
}

.sprite2 {
  background: url(sprite.png) -200px -300px no-repeat;
  width:  64px;
  height: 64px;
  /* other junk: position or float, padding, margin, font, and so on */
}

Now we use it in HTML:

<div class="sprite1">...</div>
<div class="sprite2">...</div>

We can generate a CSS like that using the default template (see options.template), or by specifying it explicitly:

// ...
var iconPath = "images/";
grunt.initConfig({
  tight_sprite: {
    my_sprite1: {
      options: {
        classPrefix: "",
        hide: iconPath,
        template: [
          ".<%= className %> {",
          "  background: url(<%= url %>) -<%= x %>px -<%= y %>px no-repeat;",
          "  width:  <%= w %>px;",
          "  height: <%= h %>px;",
          "}",
          "\n"
        ].join("\n")
      },
      src: [iconPath + "*/*.{png,jpg,jpeg,gif}"],
      dest: iconPath + "sprite.png"
    }
  }
});
// ...

But it is possible to do better than that.

Best practice: split CSS

Let's separate immutable parts common for all sprites from mutable parts, which are specific for every image, using a combo CSS above as a starting point.

Immutable (included statically, sprite.png is generated by grunt-tight-sprite):

.sprite {
  background-image: url(sprite.png);
  background-repeat: no-repeat;
  /* other junk: position or float, padding, margin, font, and so on */
}

Mutable (generated by grunt-tight-sprite):

.sprite_a {
  background-position: -100px -100px;
  width:  32px;
  height: 32px;
}

.sprite_b {
  background-position: -200px -300px;
  width:  64px;
  height: 64px;
}

As you can see we minimized junk replication.

Now we use it in HTML:

<div class="sprite sprite_a">...</div>
<div class="sprite sprite_b">...</div>

Essentially we moved from all-encompassing unique CSS classes, to a two-part declaration: this is a sprite, and we want this specific part of a sprite. The trade-off is obvious: our CSS is smaller, even with gzip, yet our HTML became more wordy. In many cases it saves space, while keeping the overhead of looking up two CSS classes reasonably low.

We can generate a CSS like that using a custom template (see options.template):

// ...
var iconPath = "images/";
grunt.initConfig({
  tight_sprite: {
    my_sprite1: {
      options: {
        classPrefix: "sprite_", // actually this is the default
        hide: iconPath,
        template: [
          ".<%= className %> {",
          "  background-position: -<%= x %>px -<%= y %>px;",
          "  width:  <%= w %>px;",
          "  height: <%= h %>px;",
          "}",
          "\n"
        ].join("\n")
      },
      src: [iconPath + "*/*.{png,jpg,jpeg,gif}"],
      dest: iconPath + "sprite.png"
    }
  }
});
// ...

Best practice: customization

Sometimes we do need to customize on a per-image basis. In this case it is usually better to mix in an additional CSS class like so (borrowing from the previous example):

.title {
  font-size:  24pt;
  font-color: blue;
}

Used like this:

<div class="sprite sprite_a title">...</div>
<div class="sprite sprite_b">...</div>

Best practice: icons

One important case of such customization is dealing with groups of images of similar sizes. In many cases we deal with sprites of a few fixed sizes, for example, icons 16 by 16, 32 by 32, 64 by 64, and so on. It opens up new opportunities to save on dynamically generated CSS.

Immutable (included statically, sprite.png is generated by grunt-tight-sprite):

.sprite {
  background-image: url(sprite.png);
  background-repeat: no-repeat;
  /* other junk: position or float, padding, margin, font, and so on */
}

.icon_32 {
  width:  32px;
  height: 32px;
}

.icon_64 {
  width:  64px;
  height: 64px;
}

Mutable (generated by grunt-tight-sprite):

.sprite_a { background-position: -100px -100px; }
.sprite_b { background-position: -200px -300px; }
.sprite_c { background-position: -100px -200px; }
.sprite_d { background-position: -200px -100px; }

Now we use it in HTML:

<div class="sprite icon_32 sprite_a">...</div>
<div class="sprite icon_32 sprite_b">...</div>
<div class="sprite icon_64 sprite_a">...</div>
<div class="sprite icon_64 sprite_b">...</div>

This style of declaration consists of three parts: this is a sprite, this is a type of image we need, and we want this particular part of a sprite. This way we keep CSS even smaller, even with gzip, with more wordy HTML. All in all we save even more.

The main drawback is that we should keep track of image sizes, and do not cut 32x32 image from a 16x16 original. In practice, the problem proved to be benign, because all mistakes are easily visible and can be corrected once occurred, and typically such assignment of CSS classes is done only once per HTML element, and left unchanged.

We can generate a CSS like that using a custom template (see options.template):

// ...
var iconPath = "images/";
grunt.initConfig({
  tight_sprite: {
    my_sprite1: {
      options: {
        hide: iconPath,
        template: ".<%= className %> { background-position: -<%= x %>px -<%= y %>px; }\n"
      },
      src: [iconPath + "*/*.{png,jpg,jpeg,gif}"],
      dest: iconPath + "sprite.png"
    }
  }
});
// ...

If for some reason we want to reduce a number of classes, we can go back to two classes by merging sprite and icon_XX together. Usually drawbacks of this approach (cutting-and-pasting sprite several times) outweigh possible performance benefits.