How Much Interface Can You Fit into a Single Icon?

Part 1

Introduction

The popularity in recent years of touch-based mobile devices has boosted the screen area available to mobile browsers, but available space is still tiny compared with desktop browsers. Mobile designers must often figure out ways to pack a great deal of functionality onto a small screen, all of which needs to be accessed with big fingertips rather than small mouse-driven pointers. We've become accustomed to a set of mobile interfaces that are patterned around this small-screen, big-finger limitation.

Mobile web interfaces typically feature a small number of site-wide navigation options available at the top of the page, with more detailed options available by drilling down to different screens. Users may then need breadcrumb hints to remind them where they are within the interface. If a page overflows with a lot of text or a long list of items, the top-level navigation options might be available only after scrolling back to the top of the page, or by getting there via a "Top of Page" link.

Now that mobile browsers support the latest set of HTML5 and CSS3 web standards, however, designers have an impressive toolkit with which to fashion complex and engaging interfaces. This article focuses on how to use CSS animations to fashion such an interface, with a goal to provide users the sense that all of a site's functionality is available at all times, much like in a desktop interface such as the one you're currently viewing.

We'll step through an example that makes common navigation, search, and sharing options all available within a single icon, one that gently inserts itself wherever users happen to navigate. When users scroll up and down within a page, the navigation icon disappears, then slides back in after a brief delay once users stop scrolling. We'll build the CSS interface from scratch, and for the sake of clarity implement it with no jQuery or any other front-end library.

This article is divided into two parts. The first part discusses a way to use animations to present the icon to the user at a fixed point on the screen. The second part discusses how to embed more options within the expanded panel, with two levels of navigation options sliding out into an accordion-style interface. The accompanying sample page shows how you might use such an interface to navigate to headings within an article — namely, the one you're reading. You can view it with WebKit-based browsers such as Safari or Google Chrome.

The techniques discussed here can make navigation on your mobile site not only more accessible, but more engaging. The overall goal for mobile web designers is to break the habit of working around low-capability browsers.

A Basic Mobile Layout

The first thing to do is structure the page and format it flexibly for mobile browsers. The structure shown here relies on semantic HTML5 block tags, but can just as easily rely on more familiar and generic div tags:

<body>
  <section id="page">
    <nav id="icon" class="scrolling">...</nav>
    <article>...</article>
  </section>
</body>
Read more about HTML5 semantic tags

Adding a viewport to the page's head region signals to the mobile browser that you want to adapt the content to the dimensions of the mobile screen, rather than letting text wrap to the very wide default window dimensions of desktop browsers:

<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no' />
Read more about the viewport meta tag

The section tag provides a common background element. It uses absolute positioning, with its top, left, and right edges snapped to the dimensions of the window:

body > section {
  position   : absolute;
  left       : 0em;
  top        : 0em;
  right      : 0em;
  background : #aaaaaa;
}

Its bottom edge is allowed to expand along with its nested contents. Unlike the body element, it also provides a reliable edge against which the enclosed article can offset a generous bottom margin to clear any browser chrome items:

body > section > article {
  padding       : 1em;
  border-radius : 1em;
  min-height    : 80%;
  margin        : 1em 1em 10em 1em;
  background    : #ffffff;
}

The article contains most of the page's content. Even when relatively empty, its min-height property makes it fill most of the screen.

A Fixed-Screen Icon?

The page represents the icon using HTML5's nav tag, which is appropriate for regions filled with navigation options. Its CSS uses absolute positioning to place the icon in the top right corner of the screen:

#icon {
  position : absolute;
  top      : 32px;
  right    : 32px;
  overflow : hidden;
}

This approach is adequate when there's not so much content on the page that the user might need to scroll downwards. In that case, the icon would scroll off the top of the screen, and users would have to return to the top of the screen to get to it. Ideally, though, you want the site's navigation elements to be available from every point within the interface, remaining stationary even as other content on the page scrolls away. Users shouldn't have to scroll or jump back to the top of the page.

CSS's position:fixed property should work in this case, by freezing the element to a set of screen coordinates rather than page coordinates. However, position:fixed is disabled on mobile browsers, due to the extra processing power it would require to scroll pages smoothly while making exceptions for individual elements.

Some JavaScript libraries offer workarounds. Cubiq's iScroll 4 and Sencha Touch address the problem by placing static elements on the screen along with elements that are made to appear scrollable in ways familiar for mobile browser users. Quick "flick" touch gestures make the element keep scrolling and eventually slow to a stop after the finger lifts from the screen. There's even a familiar bouncing effect when users reach the end of the scrolling area, which would ordinarily seem like hitting a hard wall. In this case, you would make the scrolling area fill the entire screen, then position the static icon as an overlay, but that's quite a bit of overhead for what should be a simple interface feature.

Read more about Cubiq: iScroll 4 and Sencha Touch: Mobile JavaScript Framework

As a simple alternative, the application described here maintains the single-column page layout, and repositions the icon based on how far down the page is scrolled. Manipulating the icon's top position would ordinarily appear very choppy as the JavaScript struggles to catch up with user input, but as we'll see later, CSS animations can smooth those transitions. The application described here uses a simple scroll listener to reposition the icon based on the value of window.scrollY.

Building the Application

The code implementing the interface is simple. First, set up a ui namespace to encapsulate the application. An initialization phase makes the navigation element easily accessible:

var ui = new function();
ui.delay = 2000; // ...2 seconds before returning from "scrolling" state
ui.offset = 32;  // icon's initial top offset; must match CSS

ui.init = function() {
    ui.nav = document.getElementById('icon');
};

window.onload = function() { ui.init() };

For now, the application assigns the icon one of two interface states, either static by default, or identified with a class named scrolling. To draw attention to the icon's special purpose, it slides in when the page initially loads. The initial markup defines it as scrolling, after which a delayed function returns it to its default state:

ui.isScrolling = setTimeout( ui.defaultNav, ui.delay );

ui.defaultNav = function() {
    ui.nav.className = '';
    ui.isScrolling = false;
};

Any scroll event that occurs thereafter alters the icon's top property directly, reapplies the scrolling class, and dispatches the same timer to return it to its default state:

ui.scrollNav = function() {
    ui.nav.style.top = ( window.scrollY + ui.offset ) + 'px';
    if ( ui.isScrolling ) return false;
    ui.isScrolling = setTimeout( ui.defaultNav, ui.delay );
    ui.nav.className = 'scrolling';
}

Note that the function features a test to keep from inefficiently setting off new timers while one is already in progress. After the timer expires, and while the icon slides back into the screen, the animations described in the following section smooth out transitions resulting from any subsequent scrolling.

Crafting the Animation

Once the basic class-toggling mechanism is in place, the rest of the interface can be implemented using CSS transitions. While the application mainly changes the icon's vertical position, the transitions obscure this by first sliding the icon off the screen to the right. You can do this by specifying a sequence of two transitions, with the second delayed after the first. Various -webkit-transition properties accept more than one comma-separated value:

#icon.scrolling {
  -webkit-transform                     : translateX(100px);
  -webkit-transition-property           : -webkit-transform , top;
  -webkit-transition-duration           : 0.25s             , 0.0s;
  -webkit-transition-delay              : 0.0s              , 0.25s;
  -webkit-transition-timing-function    : ease-in-out       , linear;
}

The first transition, which manipulates CSS3's translateX transform function, moves the icon to the side out of view, quite rapidly over a quarter second. The second animation, delayed until the first one ends, shifts its top position to whatever new value the scrollNav function has set it to based on the user's scroll input. Because by now the icon has been moved out of view, its duration can be safely set to zero so that the transition occurs abruptly. If the top position were not specified as part of a transition, the icon would jump just as abruptly to its new location, but before moving off-screen.

NOTE: Transitions can be flexibly applied to CSS properties (such as top) that are assigned dynamically via JavaScript, not just those that are specified within style sheets. Also note that since the horizontal positioning is prescribed and the vertical positioning is not, and because we want to manipulate them independently, they can't both be implemented as translateX and translateY transforms. You can't transition CSS's various transform functions independently of each other, so in this case the top property is necessary to move the icon vertically.

Once the icon is in its scrolling state, it returns to its default stationary state after a two-second delay plus an additional one-second transition:

#icon {
  -webkit-transform                     : translateX(0);
  -webkit-transition-property           : all;
  -webkit-transition-duration           : 1s;
  -webkit-transition-delay              : 0.0s;
  -webkit-transition-timing-function    : ease-in-out;
}

The transition back to the stationary state is far gentler than the quicker transition to the scrolling state. Setting the transition-property to all manipulates the value of any remaining property that varies from the previously applied #icon.scrolling style sheet, in this case the transform property. One of the benefits of working with CSS transitions is the ability to customize their behavior depending on whether you're moving towards or away from a particular interface state.

Read more about CSS3 Transitions and 2D Transforms.

Conclusion

So far we've seen how convenient CSS transitions are when woven into a mobile web interface. In this case, it allows the navigation icon to move to where it's needed without being too distracting. The transition back onto the screen could be made even more subtle, for example by extending its duration or by manipulating the opacity property to fade it in.

In the second part of this article, we'll see how to use transitions to implement a complex expanding panel in which you can nest site-wide functionality.

Part 2

Introduction

The first part of this article discussed a simple way to make an application's controls available to users at all times, even in the midst of long scrolling pages. This part discusses how to expand that small control into a larger panel, making many different site-wide options available. We'll focus on implementing a nested accordion interface with which to access various navigation levels at once. We'll also discuss how to make the interface gracefully dismiss itself once it's no longer needed.

The application is implemented using a little bit of JavaScript, but with most of the work done by Level 3 CSS transitions.

Expanding the Panel

So far, the application simply toggled between the navigation item's default interface state and a scrolling class, which identified when the icon needed to be repositioned. To that we'll add a third expanded state, by adding a touch handler to the navigation item, which the application has already made available as ui.nav:

ui.nav.addEventListener( 'click', ui.expandNav );

ui.expandNav = function( event ) {
    var n = event.currentTarget;
    if ( ! n.className ) event.stopPropagation();
    n.className = 'expanded';
};

Here's the relevant CSS transition:

#icon {
  -webkit-transition  : all 1s ease-in-out;
  text-align          : justify;
  background-image    : url(img/icon_expand-nav.png);
  background-position : 100% 0%;
  background-repeat   : no-repeat;
  border              : thin solid transparent;
  width               : 10%;
}

#icon.expanded {
  -webkit-box-shadow  : 0em 0em 0.5em 0.5em #aaaaaa;
  background-color    : #ffffff;
  background-position : 200% 0%;
  border-color        : #000000;
  border-radius       : 0.5em;
  width               : 80%;
}

The main transition shifts the width to expand from the size of an icon to cover most of the page. Note that the default class specifies the narrower width as a percentage rather than as the absolute size of the icon in pixels. Here, percentages offer greater flexibility to target different screen sizes, and account for the fact that transitions between fixed and flexible units don't work.

The background-image displays the default icon along the right edge. The background-position transition makes the icon fall away to the right when the panel is expanded, and slide back in when collapsed.

Additional properties provide an opaque white background and a border highlighted with a Level 3 CSS box shadow. The gray shadow is applied with no offset, but with blur and spread options that provide a backlit halo effect.

Read more about CSS3 Transitions.

Once you start testing the interface's responsiveness at this point, you may notice how well CSS transitions perform. You can interrupt or even reverse the course of a transition as it executes.

Collapsing the Panel

Most popup panels in desktop web interfaces feature an explicit control to dismiss them, usually resembling the letter X. In this mobile interface, that additional item might take up unnecessary space on the screen. Instead, this application implements a looser interface. Any tap on the screen that doesn't further expand the navigation bar causes it to collapse back down to its default icon.

The section tag that covers the entire screen serves as a handy target for a fallback touch handler, one that removes the navigation element's className to return it to its default interface state:

ui.page = document.querySelector('section');
ui.page.addEventListener( 'click', ui.defaultNav );

For this mechanism to work, the handler that initially expands the navigation panel must run stopPropagation(), to keep the event from percolating up the DOM tree and causing it to collapse again.

ui.expandNav = function( event ) {
    if ( ! event.currentTarget.className ) event.stopPropagation();
    event.currentTarget.className = 'expanded';
};

If the panel is already expanded, tapping anywhere within it sends the event up to the default touch handler by default, which collapses the panel. Regardless of whether you tap on a destination option or on an inactive portion of the screen, the panel dismisses itself. Only for handlers that call stopPropagation() does it remain open.

This simple wireframe diagram illustrates how this passive interface works:

Tapping any gray area collapses the nav element, controlled by a handler attached to the section tag. Tapping the white portion of the screen is the only way to override this default function.

Once you do so, tapping the nav in its expanded state passes the event to the default handler, which collapses it. Only by tapping newly revealed nested interface elements can you override this default function:

Adding More Options

The rest of the interface consists of repeating the same basic mechanism: expanding panels to successively reveal nested items that can also be set to expand.

A handy way to distribute icons within the expanded panel is to rely on text justification. The icons are represented here as an additional series of nested nav items, followed by a hidden span that forces them to justify:

<nav id="icon">
  <nav id="toc"></nav>
  <nav id="share"></nav>
  <nav id="find"></nav>
  <nav id="pref"></nav>
  <span class="force">&nbsp;</span>
</nav>

With the outer nav set to justify (text-align:justify) and the inner ones set to arrange horizontally like text (display:inline-block), the appended span is styled to take up a full line of its own, thus causing the previous items to distribute evenly:

span.force { margin-left : 100%; }

Note that we've already seen a transition applied when toggling the top-level navigation panel. An additional nested transition affects elements within the panel as the top-level element toggles its className, resulting in a simultaneous fade and slide-in effect:

#icon > nav {
  -webkit-transform   : translateX(300px);
  -webkit-transition  : all 1s;
  background-position : 1000% 50%;
  background-repeat   : no-repeat;
  background-size     : 80%;
  display             : inline-block;
  opacity             : 0;
  height              : 48px;
  width               : 48px;
}

#icon.expanded > nav {
  -webkit-transform   : translateX(0);
  background-position : 0% 50%;
  opacity             : 1;
}

Each navigation option, in turn, receives its own handler to expand it:

ui.opts = document.querySelectorAll('#icon > nav').toArray();
ui.opts.forEach(function(l){ l.addEventListener('click', ui.expandOpt) });

In the example above, the toArray() method is a local modification to the NodeList object the querySelectorAll() function releases. It allows you to run the JavaScript 1.6 forEach() function easily over the resulting array of elements:

NodeList.prototype.toArray = function() {
    for(var arr=new Array(),i=0,l=this.length;i<l;i++){arr.push(this[i])}
    return(arr);
};
Read more about JavaScript support in Nokia Browser 7.3.

As is true for the top-level navigation item, the handler that expands the secondary options keeps the event from percolating up to the default handler that collapses the top-level item. It also collapses other options and resizes the box to accomodate different kinds of content:

ui.expandOpt = function(event) {
    event.stopPropagation();
    ui.opts.forEach( function(l) { l.className = '' } );
    event.currentTarget.className = 'expanded';

    (this.id == 'toc' || this.id == 'pref')                                  ?
      (ui.nav.style.height = (screen.availHeight - (ui.offset * 2)) + 'px' ) :
      (this.id == 'find' || this.id == 'share')                              ?
      (ui.nav.style.height = '110px' )                                       :
      alert('failed case')                                                   ;
}

Each nested interface panel can then be placed within a div following each option.

This CSS transition makes each panel simultaneously flip and fade in:

#icon > nav + div {
  -webkit-transition : all 0.5s;
  -webkit-transform  : scaleY(0);
  opacity            : 0;
}

#icon > nav.expanded + div {
  -webkit-transform  : scaleY(1);
  opacity            : 1;
}

At the same time, the fact that the top-level icon's transition-property is set to all means that the JavaScript handler's local customizations to its height property also transition smoothly.

In this example, the interface panels accompanying each secondary navigation option include a search field, a small set of sharing options, and tab-style navigation within the site.

Accordion Sliders

The additional level of accordion-style navigation to nested headings is implemented in much the same way as the options used to access it. A touch handler toggles the className, and makes sure other top-level headings are collapsed:

ui.heads = document.querySelectorAll('#accordion > dt').toArray();
ui.heads.forEach(function(l){ l.addEventListener('click', ui.expandHead) });

ui.expandHead = function( event ) {
    event.stopPropagation();
    var selected = event.currentTarget.className;
    ui.heads.forEach( function(l) { l.className = '' });
    selected ? (event.currentTarget.className = '' )
             : (event.currentTarget.className = 'expanded');
};

The rest is handled with CSS. In this case, the accordions are implemented as data lists, with dt tags representing top-level headings, and dd tags containing lists of nested headings.

The top-level headings rely on CSS3's support for multiple background images. The first, a left-aligned icon, is toggled between the two states, while the second, a background gradient, remains constant:

#accordion > dt {
    background-image:
      url(img/icon_head-collapsed_x13.png),
      -webkit-gradient(linear, center top, center bottom, from(#aaaaaa), to(white));
    background-repeat   : no-repeat  , no-repeat;
    background-position : 0.25em 50% , 0 0;
    background-size     : auto       , auto;
}

#accordion > dt.expanded {
    background-image:
      url(img/icon_head-expanded_x13.png),
      -webkit-gradient(linear, center top, center bottom, from(#aaaaaa), to(white));
}
Read more about CSS3 Gradients and Backgrounds.

The change above occurs abruptly, while the change to the adjacent sibling element relies on the same flip-in, fade-in transition we saw before:

#accordion > dt + dd {
    -webkit-transition : all 0.5s;
    -webkit-transform  : scaleY(0);
    opacity            : 0;
    max-height         : 0;
}

#accordion > dt.expanded + dd {
    -webkit-transform  : scaleY(1);
    opacity            : 1;
    max-height         : 300px;
}

In this case, manipulating the max-height property allows the dimensions of the area containing the nested headings to vary freely. Additional styling allows the nested headings to be more easily selected as a block element, so that the finger can tap outside the text:

#accordion li > a { display : block }

For top-level headings, however, tapping outside the link text causes subheadings to expand.

Conclusion

As you can see, applying CSS transitions to successive layers of interface elements is quite easy, and facilitates engaging mobile user experiences. CSS transitions (and related keyframe animations) help provide mobile users in particular visual hints informing them how an application is responding to their input. Without them, shifts from one interface state to another can be abrupt, and users can lose context.

Read more about CSS3 Keyframe Animations.

You can expand on the basic approach described here to allow users to access account information, related items of interest, links to mobile Web Apps, or anything else that might appear along the margins of a desktop-formatted web page. You can also implement the same interface options using a more familiar mobile "drill-down" navigation from one screen to another. The benefit of this alternative approach, however, may be to unify site-wide options within a single ubiquitous interface element, one that can be styled with its own look and feel that adds a distinctive character to your mobile web-based application.