Slot Machine Tabs

Chris Coyier //

I was looking at the features page of a web service called Fluxiom. I haven't used the product (although it looks pretty nice and might be good few a couple of our clients). It's the tabs on that page that I thought were pretty neat. As you click a different tab, the three columns of text fly upward at different rates and are replaced by new columns. It looks kinda like a slot machine. I didn't investigate too deeply how they were doing it, but as I often do, I set about recreating the effect with jQuery.

After clicking a new tab the three columns slide away and are replace with new ones at random rates, like a slot machine.

View Demo   Download Files

I thought I did OK... although it can definitely be improved. There are also enough interesting things to talk about, so let's get after it.

HTML

Just going to do a bit of a code dump here so you can see it all.

<div id="slot-machine-tabs">

	<ul class="tabs">
		<li><a href="#one">Tab One</a></li>
		<li><a href="#two">Tab Two</a></li>
		<li><a href="#three">Tab Three</a></li>
	</ul>

	<div class="box-wrapper">

		<div id="one" class="content-box">
			<div class="col-one col">
				<img src="images/evangeline.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Kate</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

		<div id="two" class="content-box">
			<div class="col-one col">
				<img src="images/elizabeth.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Juliet</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

		<div id="three" class="content-box">
			<div class="col-one col">
				<img src="images/sonya.jpg" alt="" />
			</div>
			<div class="col-two col">
				<h3>Penelope</h3>
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
			<div class="col-three col">
				<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
			</div>
		</div>

	</div> <!-- END Box Wrapper -->

</div> <!-- END Slot Machine Tabs -->

Important points:

  • The whole thing is wrapped in an div with an ID value. Should we convert this idea into a plugin, the idea would be to target this ID and do the magic. But the ID is totally unnecessary for the CSS. That means we could have multiple instances of the slot machine tabs on a single page.
  • The "navigation" (the tabs) are at the top above the content boxes. With styling turned off, this will look like a navigation list like any other. The href values for the links point to the ID's of the content boxes, so the links would jump down the page to the corresponding content. Ideal.
  • When there are a lot of closing </div>'s in a row, I like to do the thing where you add a comment afterward to explain which thing this is closing (e.g. <!-- END page wrap -->)

CSS

The tabs themselves will be like any other horizontal navigation. We'll make the list items inline and the anchors block floated left. Simple borders and backgrounds, and a special state for "current" (no border and bumped down) and we're set.

.tabs { list-style: none; overflow: hidden; padding-left: 1px; }
.tabs li { display: inline; }
.tabs li a { display: block; float: left; padding: 4px 8px; color: black; border: 1px solid #ccc; background: #eee; margin: 0 0 0 -1px; }
.tabs li a.current { background: white; border-bottom: 0; position: relative; top: 2px; z-index: 2; }

One somewhat-unsemantic thing we are using is the #box-wrapper div, but whatever it's not that bad and it helps us in a number of ways. For one, it's the the relative positioning container which limits the scope of absolute positioning inside it. We can then absolutely position each content box on top of each other inside.

My favorite part is the box-shadow CSS around the #box-wrapper div. Not only does it help the tabbed area pop up a bit, but it handles the shading on the tabs themselves. The box wrapper sits on top (literally, z-index wise) of the non-current tabs. This is exactly the illusion/visual-metaphor we are going for: the current tab is on top and connected, the non-current tabs are "behind" (shadow is cast upon them).

.box-wrapper { -moz-box-shadow: 0 0 20px black; -webkit-box-shadow: 0 0 20px black; padding: 20px; background: white; border: 1px solid #ccc; margin: -1px 0 0 0; height: 210px; position: relative; }
.content-box { overflow: hidden; position: absolute; top: 20px; left: 20px; width: 658px; height: 230px; }

Notice we also pull the box up by one pixel with a negative top margin. That makes sure the under-border of the non-current tabs don't make a 2px line but a consistent 1px line.

Another important empowering concept here is that the #box-wrapper has hidden overflow and a set height. The columns in all the content boxes that are non-current are hidden by way of pushing their top value to 350px (a value taller than the height of the box). This pushes them completely out of view because of the hidden overflow. JavaScript will later do the job of pulling up new columns and pushing the old ones out of the way when needed.

The columns:

.col { width: 30%; float: left; position: relative; top: 350px; }
.col-one, .col-two { margin-right: 3%; }

Notice we only apply right margin to the first two columns. Another way to have done that is to apply the margin to all the columns but use .col:last-child { margin-right: 0; } to remove it. That might be the best way to go if you plan on having a variable number of columns. Just be aware of the lack of pseudo selector support on IE.

jQuery JavaScript

This isn't plugin-ized yet, but it probably could/should be. There are some things I would want to fix/make less redundant before that happens, which I'll cover later. You can view the full commented JavaScript file here.

I'm not going to do a full code dump but I'll cover some interesting lines.

Right away the first tab and the first content box are declared as current. The current content columns are moved to a top position of 0 (so they are visible) rather than the default hidden value from the CSS.

$(".tabs li:first-child a, .content-box:first").addClass("current");
$(".box-wrapper .current .col").css("top", 0);

We use the delegate function for the click events on the tabs, since that's so efficient (and could handle dynamic addition of tabs if that came up):

$("#slot-machine-tabs").delegate(".tabs a", "click", function() {
   // stuff
}

When a click happens, action only takes place if the tab clicked on is not the current tab and there is no other animation taking place on the page. In this limited demo, the only animation possible is the columns. In a more "real" environment the scope of this test should probably be pared down to inside the slot machine tabs specific ID. Something like $("#slot-machine-tabs *:animated)

$el = $(this);
		
 if ( (!$el.hasClass("current")) && ($(":animated").length == 0 ) ) {
    // stuff
}

I thought it was a more engaging effect if columns didn't change at the same rate each time. I set the speeds pseudo-randomly for each animation, but with a base value of half a second. The speed for the leaving column and entering column match though, so there is no overlapping.

speedOne = Math.floor(Math.random()*1000) + 500;
speedTwo = Math.floor(Math.random()*1000) + 500;
speedThree = Math.floor(Math.random()*1000) + 500;

Notice in the demo how all new columns always slide up from the bottom. But as they leave, the slide up to the top. That is accomplished because after the slide-them-up-and-away animation is finished, we instantly move them back down to the default low-and-hidden position. Before doing that, we need to make sure that all animations are completed. I had a slightly hard time doing this. Normally to fire something after an animation, it's no big deal because you can have a callback function on an animation which only fires when the animation is complete. But we are running six different animations here and because they all take a random length of time, we don't know which one is going to end last.

My solution so far is to call the same function on callback on every single finishing animation. For example:

$(".box-wrapper .current .col-one").animate({
	"top": 0
}, speedOne, function() {
	ifReadyThenReset();
});

The ifReadyThenReset() function will only do it's thing (reset the column top positions) when it's been called for the third time:

var columnReadyCounter = 0;

function ifReadyThenReset() {
	
	columnReadyCounter++;
	
	if (columnReadyCounter == 3) {
		$(".col").not(".current .col").css("top", 350);
		columnReadyCounter = 0;
	}

};

Issues:

  • It's not a plugin.
  • Each of the columns involved in a tab-changing event is individually animated. That's six things being animated at once and all of them are "hard-coded". This makes adding or removing columns more of a chore than it should be.
  • The call-the-callback-three-times thing seems kludgey to me, and the hard coded three shares the same problem as above.

Demo & Download

View Demo   Download Files

As always, do what you will with this, including use it in a corporate project to impress your boss and use as example of why you deserve a raise.