Advertisement

How to Build a Beautiful Calendar Widget

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

In today's premium tutorial and screencast, I'm going to show you how to build a slick calendar widget. We'll using CSS3 to give it a shiny look and feel, and then add some pretty neat functionality with JavaScript.


Step 0. The Idea

I follow the blog 365PSD, a really neat site that offers a free PSD—usually a little piece of UI—each day. For Day 81, there was really neat calendar widget. I figured it wouldn't be too hard to build the real thing, so I'll be showing you how to do that today!



Step 1. The HTML

We'll start off by building our HTML structure. Of course, we'll start with the skeleton:

 
	<!DOCTYPE html> 
	<html> 
	<head> 
	    <meta charset="utf-8" /> 
	    <title>Calendar Widget</title> 
	    <link rel="stylesheet" href="calendar.css" /> 
	</head> 
	<body> 
 
	</body> 
	</html>

So, inside the body, we'll start with a div to wrap it all; then, we'll have three main sections inside that:

 
	<div id="cal"> 
	    <div class="header"> 
 
	    </div> 
	    <table id="days"> 
 
 
	    </table> 
	    <div id="cal-frame"> 
 
 
	    </div> 
	</div>

First, we've got div.header; looking back at our PSD, we can see that this corresponds to the top part, the section that holds the month, the month switchers, and the bindings. Then we've got a table for the names of the days. Finally, we have a div#cal-frame. This is the calendar grid.

I'll let you in on a secret: When I originally built this calendar UI, I only had one table, with a thead for the days and a tbody for the calendar grid; but once I started writing the JavaScript to switch between months, it became apparent that I needed to use something more flexible. You'll see why when we get to the JavaScript.

So, throw this up in that header:

 
	<span class="left button" id="prev"> &lang; </span> 
	<span class="left hook"></span> 
	<span class="month-year" id="label"> June 20&0 </span> 
	<span class="right hook"></span> 
	<span class="right button" id="next"> &rang; </span>

We've got five elements here; on the outsides, we've got the left and right calendar switchers; since I didn't want to use any images in this project, I found the HTML entities &lang; and &rang ( ⟨ and ⟩, respectively ). Then, we've got two empty spans for the calendar bindings. Finally, we have the month/year label in the middle.

The content for the table#days is pretty simple:

 
 
	<td>sun</td> 
	<td>mon</td> 
	<td>tue</td> 
	<td>wed</td> 
	<td>thu</td> 
	<td>fri</td> 
	<td>sat</td>

Finally, we have the guts of div#cal-frame; check it out, and then we'll discuss it:

 
	<table class="curr"> 
	    <tbody> 
	        <tr><td class="nil"></td><td class="nil"></td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td></tr> 
	        <tr><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td class="today">11</td><td>12</td></tr> 
	        <tr><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr> 
	        <tr><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td><td>26</td></tr> 
	        <tr><td>27</td><td>28</td><td>29</td><td>30</td><td class="nil"></td><td class="nil"></td><td class="nil"></td></tr> 
	    </tbody> 
	</table>

Full Screencast



So what have we got here? Basically, we're creating the calendar grid with a table (Later, we'll insert the current month dynamically). The appropriate cells have the date numbers; if the cells are blank, they have the class "nil"; finally, today's date has the "today" class.

And really, that's the extent of the HTML; there's not much to see right now, but here's what we have:



Step 2. The CSS

Let's start with some environment:

 
	body { 
	    background: #e0e0e0; 
	} 
 
	#cal { 
	    -moz-box-shadow:0px 3px 3px rgba(0, 0, 0, 0.25); 
	    -webkit-box-shadow:0px 3px 3px rgba(0, 0, 0, 0.25); 
	    margin:50px auto; 
	    font: 13px/1.5 "Helvetica Neue", Helvatica, Arial, san-serif; 
	    display:table; 
	}

Pretty obvious, eh? After setting a background colour, we're centering the calendar widget horizontally and given it a box shadow. Of course, we set the font. But why do we set the display to table? By default, a div will display in block, which means it will take up the entire available width; by displaying it as a table, it will take up the smallest width it can (while still containing it's children), and still be a block element.

Next, let's focus on the header bar:

 
	#cal .header { 
	    cursor:default; 
	    background: #cd310d; 
	    background: -moz-linear-gradient(top, #b32b0c, #cd310d); 
	    background: -webkit-gradient(linear, left top, left bottom, from(#b32b0c), to(#cd310d)); 
	    height: 34px; 
	    position: relative; 
	    color:#fff; 
	    -webkit-border-top-left-radius: 5px; 
	    -webkit-border-top-right-radius: 5px; 
	    -moz-border-radius-topleft: 5px; 
	    -moz-border-radius-topright: 5px; 
	    border-top-left-radius: 5px; 
	    border-top-right-radius: 5px; 
	    font-weight:bold; 
	    text-shadow:0px -1px 0 #87260C; 
	    text-transform: uppercase; 
	} 
	#cal .header span { 
	    display:inline-block; 
	    line-height:34px; 
	}

Here's the first part of the header styling; we start by setting the cursor to a pointer; they way, the text won't appear to be selectable. Then, we'll set a red background colour; however, if browser supports it, we'll use a background gradient: don't forget to add it for both mozilla and webkit! Then, set the height to 34px; we'll set position to relative, because the children will be absolutely positioned; by position the parent element relatively, the children will be positioned absolutely against the parent, instead of the body. Set the text colour to white, round the top left and right corners, and make the font bold. Then, give a slight text shadow to make the text look indented. Finally, transform the text to uppercase.

Each of the items in the header is a span; each of these will be displayed as an inline block. Also, give them a line-height of 34px (the height of the header).

These spans also have some special classes, so let's look at those:

 
	#cal .header .hook { 
	    width: 9px; 
	    height: 28px; 
	    position: absolute; 
	    bottom:60%; 
	    border-radius:10px; 
	    -moz-border-radius:10px; 
	    -webkit-border-radius:10px; 
	    background:#ececec; 
	    background: -moz-linear-gradient(right top, #fff, #827e7d); 
	    background: -webkit-gradient(linear, right top, right bottom, from(#fff), to(#827e7d)); 
	    box-shadow:0px -1px 2px rgba(0, 0, 0, 0.65 ); 
	    -moz-box-shadow:0px -1px 2px rgba(0, 0, 0, 0.65 ); 
	    -webkit-box-shadow:0px -1px 2px rgba(0, 0, 0, 0.65 ); 
	} 
	.right.hook { 
	    right:15%; 
	} 
	.left.hook { 
	    left: 15%; 
	}

Firstly, we have the "hook" class; remember, these are the hooks, or bindings, of the calendar. We'll set the width and height. Then, position it absolutely. Then, we'll move it up from the bottom 60%. We'll round the corner enough to make the bindings look round. Then, we'll set a background color; if the browser supports gradients, we'll override the solid background with a gradient. Then, we'll give them a box shadow.

We'll then use the location classes to position the hooks horizontally; if the element has both the "hook" and "right" class, move it 15% from the right; if it's got the "left" class, move it 15% from the left.

Now we've got the month-switching buttons:

 
	#cal .header .button { 
	    width:24px; 
	    text-align:center; 
	    position:absolute; 
	} 
	#cal .header .left.button { 
	    left:0; 
	    -webkit-border-top-left-radius: 5px; 
	    -moz-border-radius-topleft: 5px; 
	    border-top-left-radius: 5px; 
	    border-right:1px solid #ae2a0c; 
	} 
	#cal .header .right.button { 
	    right:0; 
	    top:0; 
	    border-left:1px solid #ae2a0c; 
	    -webkit-border-top-right-radius: 5px; 
	    -moz-border-radius-topright: 5px; 
	    border-top-right-radius: 5px; 
	} 
	#cal .header .button:hover { 
	    background: -moz-linear-gradient(top, #d94215, #bb330f); 
	    background: -webkit-gradient(linear, left top, left bottom, from(#d94215), to(#bb330f)); 
	}

We'll set the width on these buttons and center the text; of course, we'll need to position them absolutely as well. Then, for the left button, we move it all the way to the left and round the top left for. For the right button, it goes to the right and rounds the top right corner.

Finally, we'll add a hover effect for the buttons; of course, we'll use a gradient.

There's one more element to style: that's the month label.

 
	#cal .header .month-year { 
	    letter-spacing: 1px; 
	    width: 100%; 
	    text-align: center; 
	}

We'll use the letter-spacing to give the characters a bit more breathing room. Then, we'll give the span a width of 100% and center the text. Because all the sibling elements are positioned absolutely, giving this the full width does exactly what we want.

So that's the whole header! I should mention that even though we've positioned the most of the elements absolutely, because we're using percentages to position them, everything scales perfectly when you raise or lower the font size in the browser.

Alright, let's move on to the day headers.

 
	#cal table { 
	    background:#fff; 
	    border-collapse:collapse; 
	} 
	#cal td { 
	    color:#2b2b2b; 
	    width:30px; 
	    height:30px; 
	    line-height:30px; 
	    text-align:center; 
	    border:1px solid #e6e6e6; 
	    cursor:default; 
	} 
	#cal #days td { 
	    height:26px; 
	    line-height: 26px; 
	    text-transform:uppercase; 
	    font-size:90%; 
	    color:#9e9e9e; 
	} 
	#cal #days td:not(:last-child) { 
	    border-right:1px solid #fff; 
	}

We start with two slightly more generic selectors: the day header and the calendar grid are both tables, so the first rule with apply to both of them: we're setting the background to white and collapsing the borders. When table borders are collapsed, they have no padding between them and adjoining cells share borders. Then, for all the table cells, we'll give them a text color, set their width, height, and line-height to 30px, and center the text. They all get a border and the default cursor (an arrow/pointer);

Then, we'll add some specific styling for the table cells in the days table: we'll reduce their height and line-height a bit, make sure they're uppercase, and reset the font size and text color. ( Note: in the accompanying screencast, I wrote #day instead of #days in the selector for the third block above and never corrected it; make sure you get it right!)

What's the final rule above for? Well, currently, there are light grey borders on the day names cells. We want to change the color of the borders on the right to white, so they aren't visible. However, we don't want to do this to the last cell in the row. So, we can use two pseudo-classes. :not will take an excluding selector "parameter." :last-child grabs the last child of the elements we've already selected: in this case, that's the table cells. Then, we just set the right border to solid white.

 
	#cal #cal-frame td.today { 
	    background:#ededed; 
	    color:#8c8c8c; 
	    box-shadow:1px 1px 0px #fff inset; 
	    -moz-box-shadow:1px 1px 0px #fff inset; 
	    -webkit-box-shadow:1px 1px 0px #fff inset; 
	} 
	#cal #cal-frame td:not(.nil):hover { 
	    color:#fff; 
	    text-shadow: #6C1A07 0px -1px; 
	    background:#CD310D; 
	    background: -moz-linear-gradient(top, #b32b0c, #cd310d); 
	    background: -webkit-gradient(linear, left top, left bottom, from(#b32b0c), to(#cd310d)); 
	    -moz-box-shadow:0px 0px 0px; 
	    -webkit-box-shadow:0px 0px 0px; 
	}

These two rules are aimed at the calendar grid. For the table cell with the class "today", we set the background to a light grey and the text to a darker grey. Next, we set a box shadow: it's a white shadow, but we're not using any blur, so it's a white line; we're pushing it up and to the right one pixel, so we get a secondary border effect. Notice that we're adding "inset" to the box shadow declaration, so that the shadow is inside the cell.

The next rule applies a hover effect to all the table cells in the calendar grid, except the ones with the with the class "nil"; we set the text to white and add a text shadow. Then, we change the background to red, using a gradient when we can. We include the box shadow removal specifically for the "today" cell.

There's a special case that we haven't mentioned yet; grab your nearest calendar—no, not iCal, I'm talking a real dead-tree paper calendar—and look at, oh, say, October 2010. You'll notice that there are the final week has a doubled-up cell, with both the 24th and the 31st in the same square. We'll need to do that, so let's style for it.

The way we'll mark that up is by putting each date in a span inside the table cell.

 
	#cal #cal-frame td span { 
	    font-size:80%; 
	    position:relative; 
	} 
	#cal #cal-frame td span:first-child { 
	    bottom:5px; 
	} 
	#cal #cal-frame td span:last-child { 
	    top:5px; 
	}

First, we position the spans relatively and shrink their font just a hair; Then, we move the first one up 5px and the second one down 5px.

We'll do one more thing; when we switch between months, we want to fade from one to the other; this requires the two tables to be on top of one another. We can achieve that with this:

 
	#cal #cal-frame table.curr { 
	    float:left;      
	} 
	#cal #cal-frame table.temp { 
	    position:absolute; 
	}

The one we're fading out will have a class of "temp"; the new one that we're bringing in to stay (for a while) will have the class "curr."

And that's it for the CSS! Now let's move on to some functionality.


Step 3. The JavaScript

We'll make the functionality for our calendar easy to reuse; in the light of that, we'll start with this skeleton:

 
	var CALENDAR = function () { 
	    var wrap, label,  
	            months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 
 
	    function init(newWrap) { 
 
	    } 
 
	    function switchMonth(next, month, year) { 
 
	    } 
 
	    function createCal(year, month) { 
 
	    } 
	    createCal.cache = {}; 
	    return { 
	        init : init, 
	        switchMonth : switchMonth, 
	        createCal   : createCal 
	    }; 
	};

So we create three functions within our CALENDAR function; one will initialize the calendar widget, the second will move between months, and the third will actually create the calendar grid; notice that line after it: createCal.cache = {}; we'll discuss that too!

We also created three variables at the top: we'll give wrap and label values within init, but months is an array with the names of the months.

Here's the content of our init function:

 
	wrap     = $(newWrap || "#cal"); 
	label    = wrap.find("#label"); 
	wrap.find("#prev").bind("click.calendar", function () { switchMonth(false); }); 
	wrap.find("#next").bind("click.calendar", function () { switchMonth(true);  }); 
	label.bind("click", function () { switchMonth(null, new Date().getMonth(), new Date().getFullYear()); });        
	label.click();

We start by giving wrap and label the appropriate values: notice that we use the selector passed to init to find wrap, but fall back to "#cal" if one isn't provided. Then, we bind click events to the next and previous calendar buttons; these call the switchMonth function; if we want the next calendar, we pass true, otherwise, we pass false.

However, switchMonth can also take two more parameters; we'll use those for the click event on the label. When the user clicks the month name, we'll switch to the current month; so, we'll pass in the current month and year, which we can get from the JavaScript Date object. Don't forget to set the next parameter to null!

One more thing (and a bonus tip, that's not in the screencast!): When the user loads the page, we want to load the correct month over the month that's hard-coded in. The simplest way to do that is to call the jQuery click method on the label without any parameters; this simulates a mouse-click, and brings the calendar to the current month.

Let's move on to the switchMonth function:

 
	var curr = label.text().trim().split(" "), calendar, tempYear =  parseInt(curr[1], 10); 
	month = month || ((next) ? ( (curr[0] === "December") ? 0 : months.indexOf(curr[0]) + 1 ) : ( (curr[0] === "January") ? 11 : months.indexOf(curr[0]) - 1 )); 
	year = year || ((next && month === 0) ? tempYear + 1 : (!next && month === 11) ? tempYear - 1 : tempYear);

We'll set a few variables at the top; we're splitting the label into an array called curr; we're also creating a calendar variable and grabbing the year of the calendar that's currently showing.

Then things get complicated. I've used JavaScript conditional operators here so I can put it all on one line. Rather than trying to explain it, check this out: this is what they're doing:

 
	if (!month) { 
	    if (next) { 
	        if (curr[0] === "December") { 
	            month = 0; 
	        } else { 
	            month = months.indexOf(curr[0]) + 1; 
	        } 
	    } else { 
	        if (curr[0] === "January") { 
	            month = 11; 
	        } else { 
	            month = months.indexOf(curr[0]) - 1; 
	        } 
	    } 
	}

You can see why the conditional operator is attractive: it's only one line. Here's the expanded version of the year variable:

 
	if (!year) { 
	    if (next && month === 0) { 
	        year = tempYear + 1; 
	    } else if (!next && month === 11) { 
	        year = tempYear - 1; 
	    } else { 
	        year = tempYear; 
	    } 
	}

At the end of it all, month and year will be the correct values for the calendar we're trying to show the user. If you'd feel more comfortable, you can replace those two lines with the snippets above.

Next we create the calendar and adjust the DOM accordingly:

 
	        calendar =  createCal(year, month); 
	        $("#cal-frame", wrap) 
	            .find(".curr") 
	                .removeClass("curr") 
	                .addClass("temp") 
	            .end() 
	            .prepend(calendar.calendar()) 
	            .find(".temp") 
	                .fadeOut("slow", function () { $(this).remove(); }); 
 
	        $('#label').text(calendar.label);

What's in the calendar object that returns from the createCal function? It's an object, like this:

 
	    { 
	        calendar : function () { /* returns a jquery object of the calendar */ }, 
	        label    : "Month Year" 
	    }

We'll discuss why the calendar property is a method when we get to building it. For now, let's return to putting it on the page. We'll get the calendar frame and find the currenly showing calendar. Then, we'll remove the class "curr" and apply the class "temp"; then we put the new calendar in (which, by the way, comes with the class "curr"), and fade out and remove the old one.

Well, we've only got one more function to go: createCal.

 
	var day = 1, i, j, haveDays = true,  
	        startDay = new Date(year, month, day).getDay(), 
	        daysInMonths = [31, (((year%4==0)&&(year%100!=0))||(year%400==0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], 
	        calendar = [];

Here's our start: the variables. We have day, set to 1; we've got two vars for iteration: i and j. Then, we figure out what day of the week the month starts on; we can do this by creating a Date object for the first day of the month and calling getDay.

Next we create an array that holds the number of days in each month; for February, we need to account for leap years, so use another ternary expression to calculated that.

Finally, we have the very important calendar variable, which is an array.

Next, we want to use that cache property we put on the createCal function. (Because everything in JavaScript is an object, even functions can have properties.)

 
	if (createCal.cache[year]) { 
	    if (createCal.cache[year][month]) { 
	        return createCal.cache[year][month]; 
	    } 
	} else { 
	    createCal.cache[year] = {}; 
	}

Here's what's going on: there's a possibility that the user will "request" the same month more than once. Once we create it the first time, there's no need to make it again; we'll put it in the cache and pull it out later.

If that cache object has a property with the name of the year we're looking for, we can then check for the availability of the month; if we've already made the month, we'll return that cached object. If there's no property for the year, we'll make it, because we'll need to put the month we're about to create in it.

If we pass this point, then we need to start creating the calendar for the requested month.

 
	i = 0; 
	while (haveDays) { 
	    calendar[i] = []; 
	    for (j = 0; j < 7; j++) { 
	        if (i === 0) { 
	            if (j === startDay) { 
	                calendar[i][j] = day++; 
	                startDay++; 
	            } 
	        } else if (day <= daysInMonths[month]) { 
	            calendar[i][j] = day++; 
	        } else { 
	            calendar[i][j] = ""; 
	            haveDays = false; 
	        } 
	        if (day > daysInMonths[month]) { 
	            haveDays = false; 
	        } 
	    } 
	    i++; 
	}

This is a complicated bit; while the haveDays variable is true, we know we have days left in the month. Therefore, we'll use our i iterator to add a week-array to the calendar array. Then, we use a for-loop on the j iterator, while it's less than 7; since we start with 0, this will give us 7 days for out week array. Inside our for-loop, there are three cases.

First, we need to check if we are in the first week of the month; if we are, we won't necessarily start on the first day. We already know what day the month starts on; that's in our startDay variable. Therefore, if j === startDay, we're on the right day to start, so we'll put the value of day in the right slot. Then we increment day and startDay by one. Next time 'round the for-loop, j and startDay will be the same, so that will continue to work for the rest of the week.

If we're not in the first week (i !== 0), then we'll make sure we still have days left to add to the calendar; if so, we slot them in place. Finally, if we're not on the first week and we don't have days left to add to the month, we'll put in a blank string instead. Then, we'll set haveDays to false.

At the end, we'll check to see if day is greater than the number of days in the month; if it is, we'll set haveDays to false. This is for the special case where the month ends on a Saturday.

Of course, don't forget to increment i just outside the for-loop!

 
	if (calendar[5]) { 
	    for (i = 0; i < calendar[5].length; i++) { 
	        if (calendar[5][i] !== "") { 
	            calendar[4][i] = "<span>" + calendar[4][i] + "</span><span>" + calendar[5][i] + "</span>"; 
	        } 
	    } 
	    calendar = calendar.slice(0, 5); 
	}

We don't want our calendar to have more than 5 weeks; if a day or two spill into week 6, we'll split the cells in week 5, as we've prepared for in our CSS. So, if there's a 6th array within the calendar array, we'll loop over it. Then, if the content of the array-element is not a blank string, we'll reassign the value of the cell directly "above" row 6: we'll wrap that value in a span and concatenate another span with appropriate value of row 6 inside. That makes sense, right?

Once we've got all in place, we'll cut the last element out of calendar.

 
	for (i = 0; i < calendar.length; i++) { 
	    calendar[i] = "<tr><td>" + calendar[i].join("</td><td>") + "</td></tr>"; 
	} 
	calendar = $("<table>" + calendar.join("") + "</table>").addClass("curr"); 
 
	$("td:empty", calendar).addClass("nil"); 
	if (month === new Date().getMonth()) { 
	    $('td', calendar).filter(function () { return $(this).text() === new Date().getDate().toString(); }).addClass("today"); 
	} 
	createCal.cache[year][month] = { calendar : function () { return calendar.clone() }, label : months[month] + " " + year }; 
 
	return createCal.cache[year][month];

Now it's time to concatenate each week in our calendar; we'll loop over each one in a for-loop and turn the entries into table rows, with each day inside a table cell. Then, we'll turn the whole thing into a jQuery object, after putting all the table rows together and wrapping them with a table. We'll then add the class "curr" to that table.

All the empty table cells (we can use the jQuery pseudo-selector :empty to find them), get the class "nil."

If we're creating a calendar for the current month, we'll find the cell for today and give it the class "today"; we can find it by passing a function to the jQuery filter method. The function returns true if the text of the cell matches the date.

Finally, we'll create our finished object and put it in the cache. Why are we making the calendar property a function? Well, if we just returned a jQuery object, once we added it to the calendar and then moved on to another month, the table would be removed from the DOM; later, if we come back to that month, the element won't show because the cache is referencing that same DOM element. So we use jQuery's clone method to return a copy of the DOM element. Then, label gets the month name from the months array and concatenated that with the year. Finally, we return the object.

We're done! Back in the index.html file, we'll add a script tag with this:

 
	var cal = CALENDAR(); 
 
	cal.init();

That's it! Here's what our finished product looks like!


But I can't show you the functionality; you'll have to check out the code yourself! Thanks for reading!

Advertisement