Combining AJAX, pushState and CSS3 transitions

I've never been entirely satisfied with the colour scheme used on this site—when rebuilding it I could never decide on a theme I was happy with but in the end settled on the deep-purplish colour you'll now only see on the home page. I've often mused about having a different theme per section but never found a good enough reason to do it until recently, when I decided to learn a little more about CSS3 transitions and HTML5 pushState, with a specific idea in mind of how I wanted to apply them to the site.

If you've arrived directly at this article, try clicking the various sections in the main navigation to see the outcome of these techniques in full effect. For example, try visiting the home page. The transitions will only work if you've got a modern browser (e.g. Chrome, Firefox, Safari, Opera etc).

Objectives

  • To add per-section themes which would be loaded asynchronously and cause a change of theme colour to ease in using CSS transitions
  • To ensure that the browser's address bar and back functionality would work as expected (e.g. what push / pop state aim to provide)
  • To degrade 100% gracefully—users without either pushState and/or CSS transitions (or JavaScript) shouldn't ever know what they're missing
  • To make creating a new theme as simple & flexible as adding a few basic CSS rules (e.g. .theme-name #header { ... }) and opting-in to that theme in a template
  • To make the site a bit faster—it's already fairly optimised, but by only re-loading the main content we should get a very slight speed boost—from the user's perspective, if nothing else.

The remainder of this article discusses the various steps undertaken to achieve the above and also highlights some of the unexpected quirks, browser bugs and other inevitable bumps in the road encountered along the way…

Step 1: PJAX support

First on the hit list was tweaking the site to respond properly to an asynchronous request for any given URL—not just any old AJAX request, but a request made with the fantastic little pjax library (written by one of GitHub's founders). PJAX provides a lightweight wrapper around AJAX requests and deals with updating the browser's push and pop states accordingly as well as performing some basic checking on the response to make sure it looks like a ‘partial’ HTML response (e.g. with no header / footer layout).

Where PJAX differs in its expectations to my usual implementation of AJAX is that it expects HTML back from the server, whereas the jaoss library assumes you want to return JSON if the usual X_REQUESTED_WITH header is present. Helpfully, pjax submits an extra header – X_PJAX. The usual headers are still sent, so any existing AJAX detection won't break; the additional header is just there to let you explicitly catch a pjax request and react accordingly if you wish to do so.

Adding this support was fairly straightforward and involved writing a few lines of library code to trap pjax requests and return a normal HTML template render, albeit with an extra Smarty variable to allow the view layer to decide whether to render a full page or just partial content. The beauty and simplicity of this approach is it lets you—as a developer—decide exactly what partial content means. For me, it was a simple case of rendering the main two-column content, the page's title tag (so pjax could grab it), and a hidden element which I could hook into to grab the theme of the new page. The full base layout template I'm using for this site can be found here, and a simplified snippet is shown below:

Although I hadn't yet built the logic to change the theme programatically, I added a blue colour scheme and some CSS transition selectors and manually toggled the <body> tag's class to check that the new theme should at least transition in once I'd written the JavaScript to look for it.

Problem #1: CSS transition limitations

It turns out CSS transitions don't yet work with background-gradients, even though they should. I tried a few of the suggested hacks and workarounds but decided that they would not be sustainable for the generic, flexible theming I was going for. I committed what I'd done and forgot all about the experiment for a while.

Step 2: CSS Transitions

Once I was reasonably happy that the basic PJAX support was working as expected, I turned my attention back to those pesky non-working transitions. There was no eventual solution to the background-gradient issue, so in the end I decided just to ditch gradients altogether and accept it as a trade-off in order to achieve what I wanted. Part of the whole experiment was to better my understanding of CSS transitions and how they could be applied over and above the commonly seen hover effects. I wasn't even sure if you could use them in the way I wanted: to transition not based on a change of state, but merely a change of class (and not only that, but indirectly—by changing a parent element's class). This section of Mozilla's CSS transitions article was immensely helpful because it instantly showed me that it was possible, albeit with a very different end result.

The approach I wanted to try was simple: change the <body> tag's class attribute, write some basic “theme” rules in my CSS (e.g. .default #header { ... }, .blue #header { ... }), and write some generic transition rules for all the elements I wanted to transition when a theme changed. An early cut of this looked a bit like the following:

I wanted the transition logic to be ‘opt-in’ (i.e. a transition rule had to be added for each set of elements I wanted it to apply to) – this approach could be a pain on sites where you want to target a wider range of or more specific selectors – but it worked for me. Well, sort of.

Problem #2: Chrome + transitions + visited links = broken

This one took me ages to work out. For some reason even though the header was transitioning correctly, some (seemingly random) links weren't—they were jumping straight to the new theme's colour scheme immediately. I tried all manner of things—from different CSS selectors, to checking that the new HTML was actually being rendered before the new theme was being applied, but nothing worked. I was stumped until I happened to see a tweet from Smashing Magazine which looked suspiciously similar to what I was currently experiencing (talk about coincidence). Sure enough, the chromium project has a bug filed about CSS transitions not working on visited links. This immediately explained why some links were transitioning but most weren't—because unsurprisingly I've visited most links on my site. I opened Firefox, manually toggled the body class and sure enough, all was well. I needed to find a way round the Chrome issue but wasn't too concerned about that for now (the solution is detailed later), as I had more basic issues to deal with.

Problem #3: Transitions being applied too liberally

I thought my aforementioned CSS selectors were pretty smart—and initially they seemed fine. However, I noticed that when hovering on / off links the colour change was also being affected by a transition, which makes sense given that my rule was targetting all A elements, irrespective of state. Making the selector a bit tighter (e.g. a:link, a:active etc) didn't entirely solve the problem either, since when you move your cursor off a link it still matches a:link, thus transitions back to its non hover state. I'd have to sort that out too, but at least things were moving in the right direction.

Step 3: Basic theming

With basic PJAX support in place and some (slightly buggy) transitions, it was time to start thinking about how to apply the over-arching concept of themes to the website. Thankfully, Smarty 3 supports blocks, a common way for templating engines to define certain sections (blocks) of content in a base layout template which child templates can override with their own content. For example, this site defines two main content blocks (one for each column) which can be overridden. The second block has some default content so that in the event of a child template not defining any content for it (like this one) the right hand column will always at least have some content.

Given that my implementation of theming was simplistic and only involved setting a class on the body tag, adding support for it was a simple case of defining a block in the base template as follows:

Overriding the theme per template was therefore equally simple:

Et voila—basic theme support. Of course this only worked for full page reloads, since we've already seen that a pjax request only renders a partial template with no body tag and therefore no theme indication. Nevertheless, this basic implementation was a step in the right direction.

Step 4: Dynamic theming

Now we're getting somewhere! CSS transition gremlins aside, this looked like it should be the final piece of the puzzle. The solution I had in mind was (as usual) simple: just render the theme block in a hidden div with a known class when rendering a pjax response and apply its value to the body element's className attribute when the end.pjax JavaScript event was fired. Omitting some irrelevant bits, it looked something like this:

Now whenever a pjax response is returned we know we can always look for an element with a class of theme (this is, upon reflection, a bit dangerous—something more likely to be unique could be used instead) and apply its value to our _body JavaScript variable—having previously used jQuery to locate the body element in the DOM and assign it to this variable. So then, that should be it, except of course it's never quite that easy.

Problem #4: Links no longer transition in Firefox

You've got to be kidding me?! The header works flawlessly, and few links I haven't visited work in Chrome, but none of the links transition in Firefox. I strongly suspected—as I had originally with Chrome's misbehaviour—that somehow the DOM wasn't being updated before the new theme was being applied, thus there were no links to transition at the time the new theme kicked in until a split-second after. However, thorough debugging didn't support this theory as the DOM did seem to have the correct contents prior to applying the theme.

Problem #5: JavaScript firing causes juddering transitions

I suspect if you only use your own lightweight scripts which do very minimal onload stuff, this wouldn't be much of an issue. However, quite a few of my pages make use of Twitter's buttons, which run some JavaScript which caused a very noticeable impact on the smoothness of the transitions (particularly in the header element, and particularly in Firefox with its slower JavaScript engine). Although not unbearable, it felt like it ruined the experience and almost the whole point of the exercise. It was time to start finding some solutions, so without further delay…

Solutions

Solution #1: background-gradient issues

As noted, there was no solution to this one (not a great start). That said, flat background colours didn't end up looking too bad, so I was happy enough to let this one go.

Solution #2: Chrome not transitioning visited links

The solution here was a mixture of ingenuity and downright dirt (aren't they always?). It's a fairly common lowbrow cache-busting technique to append a unique query string to a resource when the most up-to-date version is required—so I just pinched that and applied it to every link which was loaded in dynamically. This ensured Chrome treated each link as unvisited, thus fixing the transition issue. Of course, having links with unsightly random query strings on the end of them isn't desirable, so I hooked into the transitionend event of the header element and simply removed the query string I'd stuck on each link. Sorted. It looks a bit like this:

Solution #3: Transitions being applied too liberally

Since the way I was using transitions went hand-in-hand with JavaScript, I simply added a class of transition to the root html element in the end.pjax event handler. In the header's transitionend handler, I removed it. I then simply namespaced my CSS selectors a bit (and added support for Opera and IE10 while I was at it):

Solution #4: Firefox no longer transitioning links

I never found the scientific explanation for this, so instead settled on another bit of slight dirt: I wrapped the _body.className = theme; logic in a setTimeout() call with a minimal delay of 4ms (the lowest supported value as defined by the HTML5 specification). I wasn't massively proud of this, but it works perfectly well. Sometimes you've just got to put your ego to one side.

Solution #5: JavaScript execution interfering with transitions

In my case this was relatively simple (though it may not be for you). Since the only troublesome script was Twitter's widgets.js, I decided on only ever loading it once in the base template. This does mean that every normal page load will download and process the script, but since I use the tweet / follow buttons extensively, I can live with that. This meant that the script was no longer being re-rendered (thus processed) per pjax request, but of course did mean that the widgets didn't reload properly either. Thankfully, twitter offers a programatic way to reload any potential widgets: twttr.widgets.load(). I simply slapped that in my (already present) transitionend event handler and voila— the buttons weren't reloaded until the transition had done its work.

Conclusion

Despite the numerous challenges (there were more than those outlined, but I had to draw the line somewhere), this was not only a lot of fun, but pretty quick to build. You can see the entire commit diff for yourself, and a quick look at the affected files shows that most of the code changes were front-end oriented. In fact, if you take the jquery.pjax.js file out of the equation, the number of additions is even smaller.

Why transitions instead of JavaScript animations?

Since my use of transitions goes hand-in-hand with AJAX, and therefore requires JavaScript, you may be wondering what's wrong with jQuery's animate function. Well, nothing. Half of this was an experiment. Some of it was a learning exercise. The rest was just to make something fun and interesting. As a small aside, as far as I'm aware jQuery's animate method only works on css properties, so whilst I could have manually animated background-color and color properties of the relevant elements, adding new themes, and ensuring each theme worked without JavaScript (or on full page reloads) would have been a nightmare.

As usual, you can browse the full source code at your leisure. Likewise, any feedback, bug reports (I'm sure there are some) or improvements would be most welcome!

P.S / Problem #6: ironically, the gists embedded throughout this post turned out to be a massive pain to get working nicely with pjax. I've sort-of solved the problem, but that's a whole other article in itself…

Comments

There are currently no comments - feel free to add one!

Add a comment

Your email address won’t be published you’ll never be sent any spam. Your IP address is captured for auditing purposes and your comment will be moderated before it appears.