Corey Mwamba

menu

CSS3 pseudo-dynamic paging

Introduction

View demo

The new properties in CSS3 give the web developer more scope in presentation and design, for little effort. It is even possible to entirely replicate interfaces that before were only possible using JavaScript. This example combines two techniques;

  1. a menu that produces short summaries when the pointer hovers over the box, and
  2. text that fades in when a menu item is selected.

This would work well for a site without databases where the developer could not be certain if a user had JavaScript enabled in his or her browser. In terms of functionality it comes very close to most DHTML and some Flash versions, although the page itself is not actually dynamic (all the text is actually in the page itself and not loaded on request). There is a glaring issue with this technique with regards to document order.

Easing in gently

I suppose it's fair to ask "who's actually using this?" Well, in terms of the menu design, there's a JavaScript based example at Band On The Wall's site; it doesn't load the sections dynamically, but the interface is roughly the same. I'm not sure if it's intentional, but to my eyes the transitions are quite jerky.

Techniques for loading elements dynamically are used in all sorts of places, most commonly for photo galleries. They are usually achieved using a combination of JavaScript, PHP and XML requests (this three-pronged approach is commonly called AJAX). But if you don't know any PHP or JavaScript, you'll have to resort to copy-and-paste solutions; and these can be more problematic when there are mistakes. If the user doesn't have JavaScript or Flash enabled then that person can't use your menu. In addition, in text-based browsers and older browsers the layout can be hashed, or links cannot be accessed.

Here's the bare-bones code, as used in this document:

* {
font-size: 100%; margin: 0; padding: 0; 
font-family: "liberation sans", "nimbus sans", helvetica, "gill sans", arial, sans-serif; 
box-sizing: border-box !important;
}
body {
padding: 5em auto; width: 60em; 
margin: 1em auto;
}
h1 { font-size: 3em;padding:0; text-align: center;}
#menu {width: 80em; padding: 1ex 4em; margin: 2em auto; font-size: 0.6em; list-style: none;}
#menu li {
display: inline-block; width: auto; 
height: 16em; margin: 1ex; overflow: hidden; 
background-color: #000; 
text-align: left; float: left;
}

.lcd {
padding: 1ex; width: 16em; 
height: 10em; background-color: #000; 
color: #fff; margin: 0;
}
.lcd h2 {font-size: 2.3em; }
a:hover {color: red;}
.lcd p {display: none;}
.lcs  {font-size: 1.1em; background-color: rgb(255,125,125); padding: 1ex; width: inherit;}
.sum {padding: 1ex;  width: inherit; background-color: rgb(125,105,125); z-index: 100;}
.sum a {
height: inherit; width: inherit;
opacity: 0; display: block; color: #fff;  
text-decoration: none;
}
div {text-align: left;}
div h2 {font-size: 2.5em;}
div p {margin-bottom: 1em;}
div ol {margin: 2em auto; width: 60%; font-style: italic;}
div a {color: #000; text-decoration: underline; font-weight: 800;}
div a:hover {color: red; text-decoration: none;}
@media all and (min-width: 1px) {
.trans15 {-o-transition: all 1.5s;-webkit-transition: all 1.5s;-moz-transition: all 1.5s;transition: all 1.5s;}
.trans5  {-o-transition: all 0.5s;-webkit-transition: all 0.5s;-moz-transition: all 0.5s;transition: all 0.5s;}
.trans3  {-o-transition: all 0.3s;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;transition: all 0.3s;}
.trans2  {-o-transition: all 2s;-webkit-transition: all 2s;-moz-transition: all 2s;transition: all 2s;}
div[id^="section"] {width: 65%; margin: 1em auto; font-size: 1.2em;  position: absolute; top: 15em; left: 8em; opacity: 0; display: none;}
div#section0 {opacity:1; display: block;}
div[id^="section"]:target {opacity: 1; display: block;}
div[id^="section"]:target + div#section0:not(:target) {opacity: 0; display: none; }
.lcd {position: relative; top: -2.2em; height: 100%;}
.lcd h2 {font-size: 3em;}
.sum {position: relative; top: 20em; background-color: rgba(125,105,125,0);}
.lcs {background-color: rgba(255,125,125,0); }
#menu li:hover .lcs {color: #fff; background-color: rgba(237,105,90,0.6); width: auto;}
#menu li:hover .lcd {top: 0em; width: 18em;}
#menu li:hover .lcd p {display: block;}
#menu li:hover .lcd h2 {font-size: 1.6em;}
#menu li:hover .sum {height: 16em; top: -17em;}

}

and the structure of the document is

<ul class="lc">
<li>
<div class="lcs trans3">Strap line or tags</div>
<div class="lcd trans5">
<h2 class="trans15">Title</h2>
<p>Summary</p>
</div>
<div class="sum trans15">
<a href="#section1">link to section</a>
</div>
</li>
...
</ul>
<div id="section1" class="trans2">
...
</div>

Delving into code

So, let's start by creating a menu with the look that we want, but no bells and whistles. I'd like to have a summary for each item in the menu; and a strap-line where I could potentially put something else.

Since it's going to be a menu, then it's best to use a list: and because I want discrete blocks within each part of the menu, then it's logical to use a div inside each li, like this:

<ul>
<li>
<div class="lcs">
Strap line or tags; maybe a buy now link?
</div>
<div class="lcd">
<h2>Title</h2>
<p>Summary</p>
</div>
<div class="sum">
<a href="#section1">link to section</a>
</div>
</li>
...
</ul>

I'm using lcs for the strap-line, lcd for the description, and sum for the actual link. Note that I haven't wrapped the list inside a container: there's no need, since a ul element is a block-level element.

Then I can deal with the sections in my document. Again, each section is a discrete block, so I'm using a div for each one:

<div id="section1">
all the content goes here...
</div>

Now here's the thing to remember: with no styling, the document is already of use! Anyone can read the content. The trick to adding the styles is to ensure that this fact remains the same, no matter what we add in subsequent stages.

Let's now move to the style-sheet and how we want the interface to work. For this interface, we want to

  1. align each list item horizontally, and make each one look like a box;
  2. make each box to grow when a pointer hovers over it or when its link receives focus; and
  3. scale the size of the descriptive text and title accordingly.

To make it easier to size the menu, we can apply relative units (ems or a percentage value) for the font-size and width of the list block. You'd put the positioning values here too. Use ems if you want a fixed-size menu, and percentage values for a liquid layout.

#menu {width: 80%; font-size: 0.6em;}

Now let's create the boxes. The simplest cross-browser approach is to give each li a fixed height and automatic width; and then float them. Then we can add a margin for spacing.

#menu li {
width: auto; 
height: 16em; 
float: left;
margin: 1ex;
}

Because the li has an automatic width, it will expand depending on its contents. We only need to set the width of one of the containers; and have the other two inherit the width. Since we ideally want the descriptive text (lcs) to scale then this is the block that has the set width. We'll also set its height so the sum-classed boxes are all aligned.

.lcd {width: 16em; height: 10em;}
.lcs {width: inherit;}
.sum {width: inherit;}

Have a look at this example. It gives similar results in most graphical browsers.

Stepping it up

Now we want the items to expand on a hover - so we're using :hover on the li to give us that functionality, and altering our set-width container (lcd). This is where IE6 effectively stops, as it doesn't understand :hover on arbitary elements.

#menu li:hover .lcd {width: 20em; }

I'd like to only show the summary on the hover too. Although this affects IE6, it doesn't affect the content of the page - it can still be read, and the links are still accessible. I'm going to use a simple display: none/display: block switch:

.lcd p {display: none;}
#menu li:hover .lcd p {display: block;}

Here's the second example. Now we can add font-size declarations, so that the heading shrinks accordingly; and to help those users who might not be so hot with using a mouse, make each link into a block that fills the space of its container:

.lcd h2 {font-size: 2.5em;}
#menu li:hover .lcd h2 {font-size: 1.6em; }
.sum a {width: inherit; height: inherit; display: block;}

Here's the result. Our menu is pretty much done now! Let's leave it for now and move on to the text.

Finishing off

Our aims here are for the respective text to appear when a link is selected in the menu; and for no other text to be displayed. It's time to reflect on browser capabilities at this point, so that we can make the clearest CSS declarations that will require the least amount of tweaking. I have a method for this:

  1. write all the declarations needed to give the minimal amount of styling (we've just done this, above);
  2. in the stylesheet, make a functionally redundant media query and paste all the styles you want to improve into it; then
  3. edit and improve the styles inside the media query.

So in this case, I'd like some transitions for the expanding boxes and font size changes, and the text for each section to fade in or out when a link is selected. Since each link in the menu is heading for a named section in the document, it'd be wise to use the :target selector here: this counts out IE6 so we should put those styles using :target in the media query.

A functionally redundant media query is a declaration that in real terms makes no effect on the page; but for the styles to be applied, the browser has to understand media queries in the first place. Here's my favourite:

@media all and (min-width: 1px) {...}

Since it's a safe assumption that any medium used to look at the page will have a width wider than a single pixel, all modern browsers will apply the styles. Older ones will just skip over the block as if it didn't exist.

So, outside the media query we can define our styles for the text:

div {text-align: left;}
div h2 {font-size: 2.5em;}
div p {margin-bottom: 1em;}
div ol {margin: 2em auto; width: 60%; font-style: italic;}
div a {color: #000; text-decoration: underline; font-weight: 800;}
div a:hover { color: red; text-decoration: none;}

and then inside we deploy the CSS3! Let's set up some reusable transition classes:

.trans15 {-o-transition: all 1.5s;-webkit-transition: all 1.5s;-moz-transition: all 1.5s;transition: all 1.5s;  }
.trans5  {-o-transition: all 0.5s;-webkit-transition: all 0.5s;-moz-transition: all 0.5s;transition: all 0.5s;}
.trans3  {-o-transition: all 0.3s;-webkit-transition: all 0.3s;-moz-transition: all 0.3s;transition: all 0.3s;}
.trans2  {-o-transition: all 2s;-webkit-transition: all 2s;-moz-transition: all 2s;transition: all 2s;}

You can then add this class to all the bits where you want a gradual change, so in my case that's the heading, the descriptive text box, the strap-line and the section text:

<ul class="lc">
<li>
<div class="lcs trans3">Strap line or tags; maybe a buy now link?</div>
<div class="lcd trans5">
<h2 class="trans15">Title</h2>
<p>Summary</p>
</div>
<div class="sum trans15">
<a href="#section1">link to section</a>
</div>
</li>
...
</ul>
<div id="section1" class="trans2">
...
</div>

Now let's create a switch for the text. I'll build the statement up as we go along so you can see the reasoning.

In the document all the sections are identified with the name "section" followed by a number, so I can use the beginning sub-string attribute matching selector (^=) on the id:

div[id^="section"] {...}

We'd like a fade, so we set the opacity to zero here. But ideally, we don't want the page to jump: this is impossible to prevent in most browsers, but in Opera we have to state display: none here:

div[id^="section"] {
opacity: 0; display: none;
}

and then in the targeted section, we set the opposites of the previous declarations. For some reason, Opera is then tricked into not jumping to the position of the appearing block - other browsers are not. It's unexpected but pleasing behaviour.

div[id^="section"]:target {
opacity: 1; display: block;
}

And because it's unexpected behaviour, you can't rely on it being there at a later date - so clever positioning of all the elements in the page will be needed.

Since we want each section in the same position when it's selected we need to move it - in my case I've used absolute positioning. I've also set the width and font sizing.

div[id^="section"] {
opacity: 0; display: none;
width: 65%; margin: 1em auto; 
font-size: 1.2em;  
position: absolute; top: 15em; left: 8em;
}

And finally, let's set the strap-line to appear. In this case, I decided to position the descriptive text relatively so that it sits on top of the strap-line; and then moves downwards on the hover. Then I set the height so that it allows the link to slide up on hover.

.lcd {position: relative; top: -2.2em; height: 16em;}
#menu li:hover .lcd {top: 0em; width: 18em; height: 11em}

All that remains is to add an overflow: hidden declaration to the li, and it looks like this.

You'll notice that when the page first loads, there's no introductory text; just a menu. Assuming that the introduction is just another section (in my case, section0), it will also be invisible, according to the rules we've set in the CSS. So we have set it to display in our stylesheet:

div#section0 {opacity:1; display: block;}

and then somehow get it to vanish when another section is targeted. Unfortunately, the structure of CSS is such that you can only affect things in a cascade; so you cannot set the styles for an element based on something happening further down the document. The only to do make this work is to have the introduction at the end of the document (thereby making it a conclusion or summary!): and then, using the indirect sibling selector, writing a statement that says do this when any one of my siblings is the target, and I am not the target.

div[id^="section"]:target ~ div#section0:not(:target) {opacity: 0; display: none;}