Skip to content

Instantly share code, notes, and snippets.

@thulstrup
Created March 20, 2012 19:18
Show Gist options
  • Save thulstrup/2140082 to your computer and use it in GitHub Desktop.
Save thulstrup/2140082 to your computer and use it in GitHub Desktop.
Using Compass to generate normal and retina sprite maps
$sprites: sprite-map("sprites/*.png");
$sprites-retina: sprite-map("sprites-retina/*.png");
@mixin sprite-background($name) {
background-image: sprite-url($sprites);
background-position: sprite-position($sprites, $name);
background-repeat: no-repeat;
display: block;
height: image-height(sprite-file($sprites, $name));
width: image-width(sprite-file($sprites, $name));
@media (-webkit-min-device-pixel-ratio: 2), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 2) {
// Workaround for https://gist.github.com/2140082
@if (sprite-position($sprites, $name) != sprite-position($sprites-retina, $name)) {
$ypos: round(nth(sprite-position($sprites-retina, $name), 2) / 2);
background-position: 0 $ypos;
}
// Hard coded width of the normal sprite image. There must be a smarter way to do this.
@include background-size(444px auto);
background-image: sprite-url($sprites-retina);
}
}
// Usage.
.mail-icon {
@include sprite-background(mail);
}
@alanhogan
Copy link

I think this would be the "smarter way to do [background-size]": https://gist.github.com/2878758

@alanhogan
Copy link

You know what, I missed something. Nevermind.

@rstacruz
Copy link

I came here trying to look for a way to do "smarter background size", alas, I haven't found it yet myself.

@alanhogan
Copy link

We’ll get there! There are some smart people working on Compass extensions to do this kinda stuff automatically.

@emfeha
Copy link

emfeha commented Jul 18, 2012

Instead of hard coded width for the retina background-size, you can define
$width: image-width(sprite-file($sprites, $name));
and then just use:
@include background-size($width auto);
Tested and works :)

$sprites: sprite-map("sprites/*.png");
$sprites-retina: sprite-map("sprites-retina/*.png");

@mixin sprite-background($name) {
  background-image: sprite-url($sprites);
  background-position: sprite-position($sprites, $name);
  background-repeat: no-repeat;
  display: block;
  height: image-height(sprite-file($sprites, $name));
  width: image-width(sprite-file($sprites, $name));
  $width: image-width(sprite-file($sprites, $name));
  @media (-webkit-min-device-pixel-ratio: 2), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 2) {
    // Workaround for https://gist.github.com/2140082
    @if (sprite-position($sprites, $name) != sprite-position($sprites-retina, $name)) {
      $ypos: round(nth(sprite-position($sprites-retina, $name), 2) / 2);
      background-position: 0 $ypos;
    }
    // Hard coded width of the normal sprite image. There must be a smarter way to do this.
    @include background-size($width auto);
    background-image: sprite-url($sprites-retina);
  }
}

// Usage.
.mail-icon {
  @include sprite-background(mail);
}

@rstacruz
Copy link

No it wont work on all cases @emfeha, the background-size has to use the width of the entire sprite image, not just the single sprite. This should also take sprite padding into account.

I've dug into the Compass source to figure out how to achieve it.

.mail-button
  @include sprite(mail)
@import compass/utilities/sprites
@import compass/css3/background-size

// Define the sprites here. Notice that I've added an optional spacing.
$sprites: sprite-map("sprite/*.png", $spacing: 20px)
$sprites2x: sprite-map("sprite2x/*.png", $spacing: 40px)

// Now let's define the sprite mixin.
// This delegates to the reusable retina-sprite mixin.
@mixin sprite($name)
  @include retina-sprite($name, $sprites, $sprites2x)
// The general purpose retina sprite mixin.
//
//    @include retina-sprite(name, $spritemap1, $spritemap2)
//    @include retina-sprite(name, $spritemap1, $spritemap2[, $dimensions: true, $pad: 0])
//
//    If `dimensions` is true, then width/height will also be set.
//
//    if `pad` is non-zero, then that's how much padding the element will have (requires
//    $spacing on the sprite maps). Great for iPhone interfaces to make hit areas bigger.
//
@mixin retina-sprite($name, $sprites, $sprites2x, $dimensions: true, $pad: 0)
  @if $dimensions == true
    +sprite-dimensions($sprites, $name)

  background-image: sprite-url($sprites)
  background-position: sprite-position($sprites, $name, -$pad, -$pad)
  background-repeat: no-repeat

  @if $pad > 0
    padding: $pad

  @media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5)
    &
      $pos: sprite-position($sprites2x, $name, -$pad*2, -$pad*2)
      background-image: sprite-url($sprites2x)
      background-position: nth($pos, 1) nth($pos, 2)/2
      +background-size(ceil(image-width(sprite-path($sprites2x)) / 2), auto)
      // ^-- this is the "smarter" way to do this.
      //     sprite-path() returns the path of the generated sprite sheet, which
      //     image-width() calculates the width of. the ceil() is in place in case
      //     you have sprites that have an odd-number of pixels in width (which
      //     you shouldn't in the first place)

@capitalist
Copy link

@rstacruz - Thanks so much for your reply. I was banging my head against a wall trying to figure that out!

@Aetherpoint
Copy link

Is there any way we can not add media queries on each individual icon class on compiling? Not sure if there's a different way to go about this fix.

@media (-webkit-min-device-pixel-ratio: 2), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 2) {
     // Workaround for https://gist.github.com/2140082
    @if (sprite-position($sprites, $name) != sprite-position($sprites-retina, $name)) {
      $ypos: round(nth(sprite-position($sprites-retina, $name), 2) / 2);
      background-position: 0 $ypos;
    }
}

Ideally, the output would be something like this:

.i-rss-black {
  background-position: 0 -200px;
  height: 50px;
  width: 50px; 
}

.i-alerts-black {
  background-position: 0 -800px;
  height: 50px;
  width: 50px; 
}

.i-refresh-black {
  background-position: 0 0;
  height: 50px;
  width: 50px; 
}

.icon {
  background-image: url('../images/sd-scdd529308d.png');
  background-repeat: no-repeat;
  display: inline-block; 
}
  @media (-webkit-min-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 1.5/1), (min-device-pixel-ratio: 1.5) {
    .icon {
      -webkit-background-size: 50px auto;
      -moz-background-size: 50px auto;
      -o-background-size: 50px auto;
      background-size: 50px auto;
      background-image: url('../images/hd-s40d93ee0fe.png'); } }

Then you can just add an additional icon class:

<span class="icon i-refresh-black"></span>

@Anderson-Juhasc
Copy link

@Aetherpoint see my solution:

$sprites: sprite-map("sprites/*.png", $spacing: 20px);
$sprites2x: sprite-map("sprites2x/*.png", $spacing: 40px);

@mixin sprite-retina($name, $sprites, $sprites2x, $dimensions: true, $pad: 0) {
    background-position: sprite-position($sprites, $name, -$pad, -$pad);
    background-repeat: no-repeat;

    @if ($dimensions == true) {
        @include sprite-dimensions($sprites, $name);
    }

    @if ($pad > 0) {
        padding: $pad
    }
}

Can I create a class for sprite or improve this:

@mixin sprite-retina-class($className: sprt-bg) {
    .#{$className} {
        background-image: sprite-url($sprites);

        @media (-webkit-min-device-pixel-ratio: 2), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 2) {
            // $pos: sprite-position($sprites2x, $name, -$pad*2, -$pad*2);
            background-image: sprite-url($sprites2x);
            // background-position: nth($pos, 1) nth($pos, 2)/2;
            @include background-size(ceil(image-width(sprite-path($sprites2x)) / 2) auto);
        }
    }
}

It would be interesting to create a selector [class^="sprite-"] for all classes begin with .sprite-

Abs,

@bakura10
Copy link

bakura10 commented Sep 1, 2012

One of the above solution works really well. I have slightly modified it to have a placeholder selector :

%sprites {
  display: inline-block;
  background-repeat: no-repeat;
  background-image: sprite-url($sprites);
}

@mixin sprite($name, $dimensions: true, $pad: 0) {
  @if ($dimensions == true) {
    @include sprite-dimensions($sprites, $name);
  }

  @extend %sprites;
  background-position: sprite-position($sprites, $name, -$pad, -$pad);

  @if $pad > 0 {
    padding: $pad;
  }

  @media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5),
         (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5) {
    & {
      $pos: sprite-position($sprites2x, $name, -$pad * 2, -$pad * 2);
      background-image: sprite-url($sprites2x);
      background-position: nth($pos, 1) nth($pos, 2) / 2;
      @include background-size(ceil(image-width(sprite-path($sprites2x)) / 2), auto);
    }
  }
}

The problem is that it the background-image: sprite-url($sprites2x); makes compile time a lot longer as soon as you have a few images (as it does this for every images in retina, while it just extend for the "basic" version). Is there a solution to that ?

@AdamBrodzinski
Copy link

Just an FYI for anyone else looking to use this with a hover state, I've forked a version of this from @rstacruz and added hover and active states.
The the mixin with a demo and docs is available here: https://github.com/AdamBrodzinski/Retina-Sprites-for-Compass , as well as the mixin below.

@import "compass/utilities/sprites";         // Include compass sprite helpers
@import "compass/css3/background-size";      // Include helper to calc background size


@mixin sprite($name, $hover: false, $active: false) {
  @include retina-sprite($name, $sprites, $sprites2x, $hover, $active);
}

// The general purpose retina sprite mixin.
  //
  //    @include retina-sprite(name, $spritemap1, $spritemap2)
  //    @include retina-sprite(name, $spritemap1, $spritemap2[, $dimensions: true, $pad: 0])
  //
  //    If `dimensions` is true, then width/height will also be set.
  //
  //    if `pad` is non-zero, then that's how much padding the element will have (requires
  //    $spacing on the sprite maps). Great for iPhone interfaces to make hit areas bigger.
  //
@mixin retina-sprite($name, $sprites, $sprites2x, $hover, $active, $dimensions: true, $pad: 0) {  
  @if $dimensions == true {
    @include sprite-dimensions($sprites, $name);
  }
  background-image: sprite-url($sprites);
  background-position: sprite-position($sprites, $name, -$pad, -$pad);
  background-repeat: no-repeat;

  @if $hover == true {
    $name_hover: $name + _hover;
    &:hover {
      background-position: sprite-position($sprites, $name_hover, -$pad, -$pad);
    }
  }
  @if $active == true {
    $name_active: $name + _active;
    &:active {
      background-position: sprite-position($sprites, $name_active, -$pad, -$pad);
    }
  }

  @if $pad > 0 {
    padding: $pad;
  }

  @media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5) {
    & {
      $pos: sprite-position($sprites2x, $name, -$pad * 2, -$pad * 2);
      background-image: sprite-url($sprites2x);
      background-position: nth($pos, 1) nth($pos, 2) / 2;
      @include background-size(ceil(image-width(sprite-path($sprites2x)) / 2), auto);
      //  sprite-path() returns the path of the generated sprite sheet, which
      //  image-width() calculates the width of. the ceil() is in place in case
      //  you have sprites that have an odd-number of pixels in width

      @if $hover == true {
        $name_hover: $name + _hover;    // create myButton_hover and assign it
        &:hover{
          $pos: sprite-position($sprites2x, $name_hover, -$pad * 2, -$pad * 2);
          background-position: nth($pos, 1) nth($pos, 2) / 2;
        }
      }
      @if $active == true {
        $name_active: $name + _active;    // create myButton_active and assign it
        &:active{
          $pos: sprite-position($sprites2x, $name_active, -$pad * 2, -$pad * 2);
          background-position: nth($pos, 1) nth($pos, 2) / 2;
        }
      }
    }
  }
}

@estahn
Copy link

estahn commented Oct 5, 2012

Is there any way we can not add media queries on each individual icon class on compiling? Not sure if there's a different way to go about this fix.

I wondered the same thing. This is what i came up with: https://gist.github.com/3837343

@tegola
Copy link

tegola commented Jan 2, 2013

Do somebody have a fix for the long compile time mentioned by @bakura10? I'm using compass on a Sencha project with about 30 sprites, and while the "compass compile" takes from 15 to 20 seconds to finish – and I can see it trying to generate sprites multiple times – Sencha's build command takes forever.

@AdamBrodzinski
Copy link

@tegola , not sure it this is related but you may want to checkout this issue.

@nathansh
Copy link

I've had issues with the retina sprite when using smart layout. I tried to use smart layouts as follows:

$sprites-retina: sprite-map("double/*.png", $layout: smart);

This resulted in two identical sprite images as expected, but SASS wasn't calculating the distances properly at all.

@yahreen
Copy link

yahreen commented Apr 8, 2013

I am having issues with $layout: smartas well. Anyone have a fix?

@jessicaldale
Copy link

Anyone get '$layout: smart' working? Also, is anyone using this with images of varying sizes/dimensions? I want to create a sprite that contains all of my site's ui elements, but they aren't all square shaped.

@eprothro
Copy link

eprothro commented Nov 4, 2013

We prefer using the compass helpers for sprite map and style class generation. This is a bit more future proof than manually setting background-size as well as less labor intensive than generating the style classes manually:

Compass Retina Spriting

@heidmotron
Copy link

@thulstrup Can you include an open source license in this gist?

@brewster1134
Copy link

here is my take on retina sprites. i wanted a way to use it within the @media directive as well...

https://gist.github.com/brewster1134/13162e2cb0220e87b017

I would love some feedback...

@mbilalsiddique1
Copy link

Here is my try to make it little short and for best practice. I wanted to use background-image property for once instead of using it in every class to minimise my code. I make this
https://github.com/mbilalsiddique1/Sass-Mixins

@import "compass/utilities/sprites";         // Include compass sprite helpers
@import "compass/css3/background-size";      // Include helper to calc background size

// General Sprite Defaults
// You can override them before you import this file.
$icon-sprite-base-class: ".icon-sprite" !default;
$icon-sprite-dimensions: false !default;
$icon-spacing: 10px !default;
$icon-position: 0% !default;
$icon-repeat: no-repeat !default;

$icon-sprites: sprite-map("sprites/*.png", $spacing: $icon-spacing, $repeat: $icon-repeat, $position: $icon-position);
$icon-sprites-retina: sprite-map("sprites-retina/*.png", $spacing: $icon-spacing * 2, $repeat: $icon-repeat, $position: $icon-position);

// All sprites should extend this class
// The icon-sprite mixin will do so for you.
#{$icon-sprite-base-class} {
  background: $icon-sprites $icon-repeat;
}

@media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3 / 2), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
  #{$icon-sprite-base-class} {
    background: $icon-sprites-retina $icon-repeat;
    @include background-size(ceil(image-width(sprite-path($icon-sprites-retina)) / 2) auto);
  }
}

// Extends the sprite base class and set the background position for the desired sprite.
// It will also apply the image dimensions if $dimensions is true.
@mixin icon-sprite($name, $dimensions: $icon-sprite-dimensions, $offset-x: 0, $offset-y: 0) {
  @extend #{$icon-sprite-base-class};
  @include sprite($icon-sprites, $name, $dimensions, $offset-x, $offset-y)
}

// Example Usage.
.icon-analytics {
    @include icon-sprite(main-sprite);
}

@tomasdev
Copy link

My smaller contribution: https://gist.github.com/tomasdev/56bc62e86ab0700f8298 works right out of the box if you're planning to save only the biggest assets and not the pixelated (non-retina) ones.

@mgarabedian
Copy link

@mbilalsiddique1 - thanks for this bit above. Working well for my usage thus far. Maybe i missed it, but any way to have the css for all icons in the folder automatically render with their unique file names as classes, instead of defining each one again ( as you show in your example ) ?

// Example Usage. .icon-analytics { @include icon-sprite(main-sprite); }

@mgarabedian
Copy link

I believe i figured it out how to declare all the sprites in the map. Works well on top of whats above.

$sprite-names: sprite_names($icon-sprites);
@each $sprite in $sprite-names {
    .icon-#{$sprite} {
      @include icon-sprite($sprite, true);
    }
} 

@julkue
Copy link

julkue commented Jul 9, 2015

How to combine this mixin to get the image height of the sprite?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment