Swatch Book with CSS3 and jQuery

A tutorial about how to create a swatch book like component that let’s you open and rotate the single swatches revealing some details. We will be using CSS transforms and transitions and create a simple jQuery plugin.

Swatch Book with CSS3 and jQuery

Swatch Book with CSS3 and jQuery
Today’s tutorial is about creating an animated swatch book using CSS rotation transforms and JavaScript. The idea is to show a swatch book like structure and make the single swatches or “sheets” clickable. When clicking on a swatch, we’ll rotate the other swatches in order to reveal the selected one.

Please note: the result of this tutorial will only work as intended in browsers that support the respective CSS properties.

In this tutorial we will be using an icon font that was created with Fontello.

We will omit vendor prefixes in this tutorial. But you’ll of course find them in the files.

The Markup

For the markup we’ll have a simple structure with several divisions where each one contains an icon span and a h4:

<div id="sb-container" class="sb-container">

	<div>
		<span class="sb-icon icon-cog"></span>
		<h4><span>All Settings</span></h4>
	</div>
	
	<div>
		<span class="sb-icon icon-flight"></span>
		<h4><span>User Modes</span></h4>
	</div>	
	
	<div>
		<!-- ... -->
	</div>	
	
	<!-- ... -->
	
	<div>
		<h4><span>Profile</span></h4>
		<h5><span>We โ™ฅ color</span></h5>
	</div>
	
</div><!-- sb-container -->

The last division will not have an icon span but instead an h4 and an h5 element. This last division will be our “cover”, the top most layer of the swatch book.

Let’s take a look at the style.

The CSS

First, let’s define the style for the containing wrapper. We’ll make it the same width like one of the swatches (although they will take up more space) so that we can easily center it:

.sb-container {
	position: relative;
	width: 150px;
	height: 400px;
	margin: 30px auto 0 auto;
}

Our aim is to create a swatch book like structure with several swatch “sheets”. Each one will be rotated using the CSS transform property (JS). So, let’s give the swatches a realistic width and height and make them absolute. Our initial state is that all swatches are stacked on top of each other. The transform-origin will define how our swatches will be rotated. Since we’ll want to use the bottom left corner for that, we’ll set a value of 25% 90%. The backface-visibility hidden will help avoiding a jagged looking text when rotating:

.sb-container div {
    position: absolute;
	top: 0;
	left: 0; 
	width: 130px;
	background: #fff;
	height: 400px;
	border-radius: 5px;
	cursor: pointer;
	text-align: center;
	background-image: url(../images/fabric.png);
	transform-origin: 25% 90%;
	backface-visibility: hidden;
}

Let’s define a different background color and box shadow for each division:

.sb-container div:nth-child(1){
	background-color: #ea2a29;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 1px 1px 1px rgba(0,0,0,0.1);
}
.sb-container div:nth-child(2){
	background-color: #f16729;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 2px 2px 1px rgba(0,0,0,0.1);
}
.sb-container div:nth-child(3){
	background-color: #f89322;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 3px 3px 2px rgba(0,0,0,0.2);
}
.sb-container div:nth-child(4){
	background-color: #ffcf14;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 4px 4px 4px rgba(0,0,0,0.2);
}
.sb-container div:nth-child(5){
	background-color: #ffea0d;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 5px 5px 6px rgba(0,0,0,0.3);
}
.sb-container div:nth-child(6){
	background-color: #87b11d;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 6px 6px 8px rgba(0,0,0,0.3);
}
.sb-container div:nth-child(7){
	background-color: #008253;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 7px 7px 10px rgba(0,0,0,0.4);
}
.sb-container div:nth-child(8){
	background-color: #3277b5;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 8px 8px 12px rgba(0,0,0,0.4);
}
.sb-container div:nth-child(9){
	background-color: #4c549f;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 9px 9px 14px rgba(0,0,0,0.4);
}
.sb-container div:nth-child(10){
	background-color: #764394;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 10px 10px 16px rgba(0,0,0,0.4);
}
.sb-container div:nth-child(11){
	background-color: #ca0d86;
	box-shadow: -1px -1px 3px rgba(0,0,0,0.1), 11px 11px 18px rgba(0,0,0,0.4);
}

We want to make it look as realistic as possible, so we’ll give the bottom most element, which is our first child, a very subtle shadow. For every following element we’ll increase that second shadow.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

The last swatch will serve as a cover, so we’ll give it a special background and box shadow:

.sb-container div:last-child{
	background: #645b5c url(../images/cover.jpg) repeat center center;
	box-shadow: 
		-1px -1px 3px rgba(0,0,0,0.2),
		12px 12px 20px rgba(0,0,0,0.6),
		inset 2px 2px 0 rgba(255, 255, 255, 0.1);
}

Let’s add a little brass fastener. For that we’ll use the pseudo-class :after. It will have a gradient and a box shadow to make it look like as if it’s from metal. The position depends on the transform-origin of the swatches and since we’ve chosen the bottom left corner, we’ll set the according position of the brass fastener to be there, too:

.sb-container div:last-child:after{
	content: '';
	position: absolute;
	bottom: 15px;
	left: 15px;
	width: 20px;
	height: 20px;
	border-radius: 50%;
	background: #dddddd;
	background: linear-gradient(135deg, #dddddd 0%,#58535e 48%,#889396 100%);
	box-shadow: -1px -1px 1px rgba(0,0,0,0.5), 1px 1px 1px rgba(255,255,255,0.1);
}

Let’s style the headings:

.sb-container div h4{
	color: rgba(255,255,255,0.9);
	text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
	font-weight: 700;
	font-size: 16px;
	text-transform: uppercase;
	border-top: 1px dashed rgba(0,0,0,0.1);
	border-bottom: 1px dashed rgba(0,0,0,0.1);
	margin: 5px;
	padding: 5px;
	user-select: none;
}
.sb-container div:last-child h4{
	background: rgba(0,0,0,0.2);
	box-shadow: 0 1px 1px rgba(255,255,255,0.1);
}

The big heading on the cover will be rotated and translated into the right position. This depends on the size of it, so if we were to use other words, we’d need to adjust these values:

.sb-container div:last-child h5{
	font-size: 50px;
	white-space: nowrap;
	text-align: left;
	margin: 0;
	padding: 0;
	position: absolute;
	line-height: 50px;
	top: 0px;
	left: 0px;
	color: #111;
	text-shadow: -1px -1px 1px rgba(255,255,255,0.1);
	text-transform: uppercase;
	transform: rotate(-90deg) translateX(-157%) translateY(73px);
	transform-origin: 0 0;
	user-select: none;
}

Last, but not least, let’s style the icon class. We’ll use an icon font that we have generated with Fontello (Entypo icon set). We’ll style the span and the :before pseudo-element, which will contain the characters of the icon font:

span.sb-icon{
	display: block;
	height: 70px;
	width: 70px;
	margin: 20px auto;
	user-select: none;
}
span.sb-icon:before {
	font-family: 'icons';
	font-style: normal;
	font-weight: normal;
	speak: none;
	display: block;
	text-decoration: inherit;
	text-align: center;
	text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); 
	line-height: 64px;
	width: 100%;
	font-size: 60px;
	color: #000;
	text-shadow: 0 0 1px #000;

}
.icon-cog:before { content: '35'; } /* '5' */
.icon-flight:before { content: '37'; } /* '7' */
.icon-eye:before { content: '34'; } /* '4' */
.icon-install:before { content: '39'; } /* '9' */
.icon-bag:before { content: '36'; } /* '6' */
.icon-globe:before { content: '38'; } /* '8' */
.icon-picture:before { content: '32'; } /* '2' */
.icon-video:before { content: '30'; } /* '0' */
.icon-download:before { content: '41'; } /* 'A' */
.icon-mobile:before { content: '42'; } /* 'B' */
.icon-camera:before { content: '33'; } /* '3' */

And that’s all the style! Now, let’s make some magic!

The JavaScript

Let’s first take a look at our plugin options:

$.SwatchBook.defaults = {
	// index of initial centered item
	center : 6,
	// number of degrees that is between each item
	angleInc : 8,
	speed : 700,
	easing : 'ease',
	// amount in degrees for the opened item's next sibling
	proximity : 45,
	// amount in degrees between the opened item's next siblings
	neighbor : 4,
	// animate on load
	onLoadAnim : true,
	// if it should be closed by default
	initclosed : false,
	// index of the element that when clicked, triggers the open/close function
	// by default there is no such element
	closeIdx : -1,
	// open one specific item initially (overrides initclosed)
	openAt : -1
};

The options mean the following:

  • center: The index of the “centered” item, the one that will have an angle of 0 degrees when the swatch book is opened
  • angleInc: The space between the items (in degrees)
  • speed & easing: The speed and transition timing functions
  • proximity: The distance from the opened item to its next sibling
  • neighbor: The distance from the opened item’s next siblings
  • onLoadAnim: If true the swatch book will open with a transition on load, otherwise it will be opened by default
  • initclosed: If true the swatch book will be initially closed
  • closeIdx: The index of the item that will trigger the swatch book’s close function when clicked
  • openAt: The index of the item that will be opened initially

We will start by executing the _init function:

_init : function( options ) {
			
	this.options = $.extend( true, {}, $.SwatchBook.defaults, options );

	this.$items = this.$el.children( 'div' );
	this.itemsCount = this.$items.length;
	this.current = -1;
	this.support = Modernizr.csstransitions;
	this.cache = [];
	
	if( this.options.onLoadAnim ) {
		this._setTransition();
	}

	if( !this.options.initclosed ) {
		this._center( this.options.center, this.options.onLoadAnim );
	}
	else {

		this.isClosed = true;
		if( !this.options.onLoadAnim ) {
			this._setTransition();
		}

	}

	if( this.options.openAt >= 0 && this.options.openAt < this.itemsCount ) {
		this._openItem( this.$items.eq( this.options.openAt ) );
	}
	
	this._initEvents();
	
}

Here we are basically caching some elements and initializing some variables to be used later.
Also, we need to check if the swatch book will be initially opened or closed. If it should be closed, we need to set the CSS transitions for the items (_setTransition function).

Finally we load the click events on the items.

_setTransition : function() {

	if( this.support ) {
		this.$items.css( { 'transition': 'all ' + this.options.speed + 'ms ' + this.options.easing } );
	}

}

When we click on one of the items, the item will rotate so it has 0 degrees. We also need to rotate its siblings, in such way that we can read the opened item’s content:

_initEvents : function() {

	var self = this;
	
	this.$items.on( 'click.swatchbook', function( event ) {
		self._openItem( $( this ) );
	} );

}

_openItem : function( $item ) {
	
	var itmIdx = $item.index();
	
	if( itmIdx !== this.current ) {

		if( this.options.closeIdx !== -1 && itmIdx === this.options.closeIdx ) {

			this._openclose();
			this._setCurrent();

		}
		else {

			this._setCurrent( $item );
			$item.css( { 'transform' : 'rotate(0deg)' } );
			this._rotateSiblings( $item );

		}

	}

}

The _rotateSiblings looks as follows:

_rotateSiblings : function( $item ) {

	var self = this,
		idx = $item.index(),
		$cached = this.cache[ idx ],
		$siblings;

	if( $cached ) {
		$siblings = $cached;
	}
	else {

		$siblings = $item.siblings();
		this.cache[ idx ] = $siblings;
		
	}

	$siblings.each( function( i ) {

		var rotateVal = i < idx ? 
			self.options.angleInc * ( i - idx ) : 
			i - idx === 1 ?
				self.options.proximity : 
				self.options.proximity + ( i - idx - 1 ) * self.options.neighbor;

		var transformStr = 'rotate(' + rotateVal + 'deg)';

		$( this ).css( { 'transform' : transformStr } );

	} );

}

Basically, we are rotating the item’s next siblings in a way that these leave space for us to read the content of the item. The amount itself is defined in the options (proximity and neighbor).

If we want the swatch book to be initially closed and/or we specify an item to trigger the open/close function, the click event on that item will open/close the swatch book, depending on its current status:

_openclose : function() {

	this.isClosed ? this._center( this.options.center, true ) : this.$items.css( { 'transform' : 'rotate(0deg)' } );
	this.isClosed = !this.isClosed;

}

And that’s all, I hope you like it and find it inspiring!

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animationsโ€”zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 53

Comments are closed.
  1. Awsome ๐Ÿ™‚ very creative and very usefull to use in our next design projects! ๐Ÿ™‚

  2. Mary! You are amazing, i have never seen a developer using css3 like you!!

  3. When using Firefox 13.0.1 if I click on either of the slides with text the navigation stops working.

  4. Manoela. I wonder where you can still take us! you work creativiti i simple amazing, awesome job!

  5. Mary Lou when are you going to come out with a book or even better a paid video tutorial service? Perhaps Codrops extended? I would def pay for that! I love you!!!

  6. I would like to insert links as well, but it doesn’t work the way i want it to. I would like to click on a card and than click the name to go to that link, how do i do that?

  7. Brilliant work Lou! I can envision combining this with lightbox to create a truly stunning portfolio website. Your imagination never ceases to amaze me. A true inspiration. keep up the good work.

  8. Great tutorial, the demo you have posted has already got me thinking about the possibilities for creating websites with unique navigation bars.
    This system could also be used as an alternative to jQuery tabs, which are becoming more and more common in modern websites. The only downfall as ever is the lack of compatibility with internet explorer.

  9. U know Manoela? Whenever I receive an email from codrops and I see something this creative, I instantly know that it’s an article written by you.. you’re super creative I can’t believe it ๐Ÿ™‚ and super good with css and jquery.. how did u get this good ??? any special learning resources u recommend ?? I hope u reply ๐Ÿ™‚

  10. Thank you all for your great feedback! I’m happy that you like it ๐Ÿ™‚
    @Ruslan @Rob I’ve updated the ZIP file with an adjusted version of the script which allows the usage of links.
    @Sara I mostly learned “by doing” ๐Ÿ™‚ No special learning resources really, just the standard references. I think a good way to learn is to try and solve problems or create something ๐Ÿ™‚
    Cheers, ML

    • Thank you for replying ๐Ÿ™‚
      Well.. for me, the “current” best way for learning is by studying your tutorials ๐Ÿ™‚ and you’re right, practice makes perfect, I just hope I’ll get as good as u someday ๐Ÿ™‚
      Cheers ๐Ÿ™‚

  11. Hey Manoela, nice one ! This tutorial is a door opened for our web creation flows. ๐Ÿ˜€
    I’d like to ask you if there is a way to make this transition works in IE9?

  12. Excellent work. Many dedicaciรณne inventiveness. Thanks for sharing;-)

    Greetings.

    Daniel.

  13. Nice idea and great tutorial. I can see this being used in a tool for a similar color picker…like 0to255.

  14. Laggy in Gecko, stunning in Webkit, who cares about IE. Great use of CSS and jQ.

  15. I made some changes to this plugin:
    – i added new method called ‘_openCurrent’ which makes specified item to be opened by default
    – i added new option ‘cItem’ which specifies number of item
    here is code of method
    _openCurrent : function() { var _self = this; _self.$items.each(function(i) { if (i == _self.options.cItem){ var transformStr = 'rotate(0deg)'; $(this).css( { '-webkit-transform' : transformStr, '-moz-transform' : transformStr, '-o-transform' : transformStr, '-ms-transform' : transformStr, 'transform' : transformStr } ); _self._rotateSiblings( $(this) ); } }) }

    then we call this method from _initEvents after var _self = this;
    var _self = this; _self._openCurrent();

    and we have to add option cItem into $.SwatchBook.defaults
    $.SwatchBook.defaults = { // index of initial centered item center : 3, // number of degrees that is between each item angleInc : 8, speed : 700, easing : 'ease', // amount in degrees for the opened item's next sibling proximity : 45, // amount in degrees between the opened item's next siblings neighbor : 4, // animate on load onLoadAnim : true, // if it should be closed by default initclosed : false, // index of the element that when clicked, triggers the open/close function // by default there is no such element closeIdx : -1, cItem : -1 };

  16. Great Work! Thanks a lot for sharing!

    still dont understand how to make links working. sorry ๐Ÿ™

  17. For LINKS you can use onclick event

    span onclick=”top.location=’http://www.google.com'”>Digital App</span

    Hope this helps

  18. This is one of the Greatest websites I have ever come across. You guys really know your stuff. I learn so much from this site. Well Done CoDrops:)

  19. I am completely blown away with you original thinking and unique way of design. keep it up ๐Ÿ™‚

  20. Good stuff! Iยดm gonna use it on my site, but I will code a proper css fallback to the damn IE 8 because many people still use it (here in Argentina) and those red legends are not a proper solution. Keep these stuff coming!!

    • Jajajaja, es cierto, explorer en argentina nos da de comer a todos los que reparamos pcยดs en este pais! jajajaja muy bueno! esto es una idea explotada al maximo!

      Salud!

  21. Hi Mary….

    Thanks again for a lovely tut. Tried to implement on my site but had some issues… (www.habariconsulting.com/ep)

    Was trying to come up with an overlay state for each button and to have it fixed.. could anyone help?

  22. This is excellent!
    I’m going to use this to display some images. I’ve successfully dropped in all my media and it looks great…but is there a way to responsively scale everything depending on browser width?

  23. Thanks for this beautiful plugin !
    It works like a charm, and has a very good visual impact on visitors !
    I’m a painter, so this “menu” is perfect for my different sections !!

    Here is my version: http://etien.fr/galeries/

    Thanks again and merry x-mas ๐Ÿ™‚