Slider with Sliding Backgrounds

Published by Chris Coyier

Among the many super nice design features of the Yahoo! Weather app for iOS is the transition between city screens. The background image doesn't just move away as the screen moves from one screen to the next, the background image itself slides. It appears to be hiding some of the "old" screen and revealing more of the "new" screen those closer you have it to being in full view.

Let's try and pull it off on the web.

The HTML

Like any slider, there are three main components:

  • The container that holds everything into shape
  • A sliding container that is as wide as all the slides in a row
  • Each individual side container

We won't bother too much with content inside the slide. I'll just add the temperature to show each slide can indeed hold content on top.

<div class="slider" id="slider">
  <div class="holder">
    <div class="slide" id="slide-0"><span class="temp">74°</span></div>
    <div class="slide" id="slide-1"><span class="temp">64°</span></div>
    <div class="slide" id="slide-2"><span class="temp">82°</span></div>
  </div>
</div>

The container might be a <section>, slides might be <article>. It really depends. I'll let you make the semantic choices for your own needs.

The layout plan is like this:

The CSS

The "slider" (visual container) and the slides need to have explicity the same size. We'll use pixels here but you could make it work with anything.

.slider {
  width: 300px;
  height: 500px;
  overflow-x: scroll;
}
.slide {
  float: left;
  width: 300px;
  height: 500px;
}

Floating those slides to the left isn't going to make them line up in a row, because the parent element of the slides isn't wide enough to let them do that. That's one of the reasons we need the holder element. It will be 300% wide (Num. of slides × 100%) which will fit three slides exactly.

.holder {
  width: 300%;
}

Each one of our slides has a unique ID. This is useful because, if we choose, we can create anchor links that link to those ID's and the slider will "jump" to those slides. We'll add JavaScript to do some actual "sliding", but our slider will work even without that. ID's make that possible, so let's use them here to drop in some lovely background images.

#slide-0 {
  background-image: url(http://farm8.staticflickr.com/7347/8731666710_34d07e709e_z.jpg);
}
#slide-1 {
  background-image: url(http://farm8.staticflickr.com/7384/8730654121_05bca33388_z.jpg);
}
#slide-2 {
  background-image: url(http://farm8.staticflickr.com/7382/8732044638_9337082fc6_z.jpg);
}

With all this in place, our layout comes into shape:

The CSS (black fading)

Just as a small detail, the temperature set in white may be in danger of not being visible depending on the photo behind it. To ensure that it is, we can make the photo subtly fade to black toward the bottom. A pseudo element will do nicely.

.slide:before {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 40%;
  background: linear-gradient(transparent, black);
}

A picture explanation is in order here:

The JavaScript (background sliding)

We're going to use jQuery here because we love life. Our goal is the adjust the background-position of the slides as we scroll. We can set background-position in percentages in CSS, but that alone doesn't do the cool hide/reveal more effect we're looking for. Based the amount scrolled (which we can measure in JavaScript), we'll adjust the background-position. Alone, that would look something like this:

$("#slider").on("scroll", function() {
  $(".slides").css({
    "background-position": $(this).scrollLeft()/6-100+ "px 0"
  });  
});

The "6" and "-100" in there are magic numbers. Not CSS magic numbers that are prone to frailty, but traditional magic numbers. Just some numbers that happen to make the effect work. A bummer, perhaps, but not that big of a deal. Design-y things are sometimes like that. Perhaps best to leave a comment in the code to that effect. These particular numbers are based on the images I used and their size and what looked good.

The effect here is the background shifting we're after:


Notice the less of the yellow streetcar is visible when the slide is almost out of view.

The JavaScript (imparting structure)

That little snippet of JavaScript looks lonely up there without any structure behind it. A slider is a great excuse to look at a simple way to structure JavaScript.

We can make everything slider-related one object.

var slider = {

};

Then we'll group up the related elements into one area, bind our events together, and write little functions that do very specific things.

var slider = {
  
  el: {
    slider: $("#slider"),
    allSlides: $(".slide")
  },
 
  init: function() {
    // manual scrolling
    this.el.slider.on("scroll", function(event) {
      slider.moveSlidePosition(event);
    });
  },
  
  moveSlidePosition: function(event) {
    // Magic Numbers
    this.el.allSlides.css({
      "background-position": $(event.target).scrollLeft()/6-100+ "px 0"
    });  
  }
  
};

slider.init();

The HTML (adding navigation)

Adding swipe stuff would be super (super) sweet (hint). But for now let's add little press-able links to change slides, rather than relying on the scrollbar. You might even remove the scrollbar in real life (straight up overflow: hidden; on the container). What we need is anchor links that link to the ID's of the individual slides.

<nav class="slider-nav">
  <a href="#slide-0" class="active">Slide 0</a> 
  <a href="#slide-1">Slide 1</a> 
  <a href="#slide-2">Slide 2</a> 
</nav>

Style those as you will. For the demo, I make them little tiny gray circles with the text hidden.

The JavaScript (adding navigation)

Our structure is more useful now. We simply add a few more elements we're dealing with, add a new event we're watching for (clicks on nav), and write a little function to deal with that event.

We know how far to animate the scroll position when a nav link is clicked from the ID on the link itself. The link might be href="#slide-1", which we can get "1" from easily. Then the position we need to scroll to is (1 × width of slide), so 300 in our case. We'll store that 300 value right in the JavaScript.

var slider = {
  
  el: {
    slider: $("#slider"),
    allSlides: $(".slide"),
    sliderNav: $(".slider-nav"),
    allNavButtons: $(".slider-nav > a")
  },
  
  timing: 800,
  slideWidth: 300, // could measure this
  
  init: function() {
    // You can either manually scroll...
    this.el.slider.on("scroll", function(event) {
      slider.moveSlidePosition(event);
    });
    // ... or click a thing
    this.el.sliderNav.on("click", "a", function(event) {
      slider.handleNavClick(event, this);
    });
  },
  
  moveSlidePosition: function(event) {
    // Magic Numbers
    this.el.allSlides.css({
      "background-position": $(event.target).scrollLeft()/6-100+ "px 0"
    });  
  },
  
  handleNavClick: function(event, el) {
    // Don't change URL to a hash, remove if you want that
    event.preventDefault();

    // Get "1" from "#slide-1", for example
    var position = $(el).attr("href").split("-").pop();
    
    this.el.slider.animate({
      scrollLeft: position * this.slideWidth
    }, this.timing);
    
    this.changeActiveNav(el);
  },
  
  changeActiveNav: function(el) {
    // Remove active from all links
    this.el.allNavButtons.removeClass("active");
    // Add back to the one that was pressed
    $(el).addClass("active");
  }
  
};

slider.init();

We have an "active" class on the nav links just to use in CSS to visually indicate which slide is active. We handle that by removing "active" from all links and then adding it back to the one that was clicked.

Demo!

Check out this Pen!