CSS3 :target based interfaces

By Corey Mwamba

Introduction

The new properties in CSS3 give us more scope for creating more powerful design features with very little effort, many of which were previously only available through JavaScript or Flash. This example uses the :target selector, a healthy dose of CSS3 transitions and the general sibling combinator to create a modern user interface. You can see the end result here.

  1. Background information
  2. A look at the example code
  3. Explanations
  4. Issues with spiders and reptiles
  5. Conclusion

Background information

Techniques for scrolling elements dynamically are used in all sorts of places. They are usually achieved using JavaScript, or designed in a pure graphical format like Flash. But what if you don't want to use Flash or JavaScript? With this pure CSS3 solution, a developer/designer has a fall-back or replacement solution.

Katherine Cory's site has a fascinating interface. The eye is guided across a large surface to the section, rather than a static loading of the next section. The method used is JQuery: three fairly short scripts that nevertheless are beyond my knowledge of Javascript. In addition, my friend natureboy's site scrolls horizontally, but it is rendered entirely in Flash. With the new additions to CSS3 it is entirely possible to render a similar horizontally scrolling interface in HTML and CSS, without the use of proprietary software; and this is what we shall do!

A look at the example code

To start off, we'll have a look at the final example we're working towards in this article. Note that in this example vendor-specific prefixes (-o-, -moz-, -webkit-) have been left out for brevity. You can view the final CSS and view the final HTML in separate pages (they are quite long listings.) You can also view the final example running live.

Explanations

Now we've looked at the example code, let's look at some background information, and talk about how to create it, step by step.

First principles: a simple example

In a previous article, we looked at how we could expand and collapse elements in a web-page using the :focus selector to trigger the switch. This technique has quite a few benefits - the browsing history is not changed by using the :focus switch, and it replicates the behaviour of many applications. For example, if you click on any item in your menu bar then move away from it and click inside the window, the menu will disappear.

However, if you want a more permanent change upon click, the use of :focus falls down. As we said at the end of the last article, when you lose focus, everything changes. If you need a persistent change, then :target comes into play. While :focus selects an element that has had some interaction from a user, :target matches the fragment identifier for a specific element, so you can set elements to be styled a certain way only when they are linked to using a fragment identified, and the link in question is clicked.

The fragment identifier is contained in the ID attribute, and affects the browsing history, so for example if you were at http://www.example.org/index.html, the link <a href="http://www.example.org/index.html#end">go to end</a> would take you to an element with an id of "end", and be displayed in the address bar as http://www.example.org/index.html#end, which a browser treats as a different location.

So, if we had a page containing this simple content:

<ul>
   <li><a href="#s1">S1</a></li>
   <li><a href="#s2">S2</a></li>
</ul>
<p id="s1">hello</p>
<p id="s2">bye</p>

You could apply a style to the paragraphs to hide them and then display them and move them when targeted. In this case we want to move each one from a different direction and make the text fade in and out. Let's style the paragraphs when they are not targeted first: since we want each paragraph to behave differently we need to style each one separately (proprietary prefixes for transition are left out for brevity).

p[id^="s"] {
  width: 5em;
  height: 5em;
  position: absolute;
  font-size: 4em;
}

p#s1 {
  left: 21em;
  top: 2em;
  opacity: 0;
  transition: all 4s;
}

p#s2 {
  left: 1em;
  top: 2em;
  opacity: 0;
  transition: all 4s;
}

Note: In HTML5, you can start ID values off with numbers if you so wish. This wasn't allowed before, in HTML4.

Now let's set the paragraphs to fade in and go to a specific position when they are the target of the fragment in the current URL:

p#s1:target, p#s2:target {
  left: 14em;
  top: 2em;
  opacity: 1;
}

see our first simple example in action here, and notice how (depending on the resolution of your screen) the page gains a horizontal scroll bar. The browser (correctly) jumps to the target, but in our case we don't want this correct behaviour as it's quite ugly. Luckily this is easy to fix: apply position: fixed; to the containing block (in this case, the body):

body {
  margin: 0;
  padding: 0;
  font-size: 100%;
  font-family: freesans;
  position: fixed;
}

A fixed position element is placed relatively to the browser window. Since in this case the containing block is the body, we are effectively fixing the body in place. This breaks the normal targeting behaviour of the browser and the window will not jump or scroll.

Heading towards real life: horizontal targeting and backgrounds

In the short previous example we have the basis of the technique. Now we can apply it to a more common page structure: it's rare that we would fix the position of the body itself; it's much more likely that we would have our elements in a container. So let's now create a page with an article, and sections:

<ul id="nav">
  <li><a href="#s0">Welcome</a> </li>
  <li><a href="#s1">Tab styling</a></li>
  <li><a href="#s2">Applications</a></li>
</ul>

<article>

  <section id="s0">
    <h2>Welcome</h2>
    <p>This is a basic study in green, in HTML5. I've decided to use
    a collection of <code>section</code> elements housed within
    an <code>article</code>, but I could have used a generic
    container (<abbr title="id est" lang="la">i.e.</abbr> a
    <code>div</code>). As long as the outer container (in this
    case <code>article</code>) holds the background image and is
    fixed, then you can get a scrollable background!</p>
  </section>

  <section id="s1">
    <h2>Tab styling</h2>
    <p>It's all rounded corners, and an inset <var>box-shadow
    </var> to create a three-dimensional effect. It perhaps is not as
    good as a drawing - but it's quicker.</p>
  </section>

  <section id="s2">
    <h2>Applications</h2>
    <p>You could use this for a kiosk or information terminal; and bearing
    in mind that you don't only have to scroll horizontally, a very creative
    person could create an accessible web-comic where the reader was guided to
    each frame, with the art rendered in SVG, or text with
    <var>:content</var> generated images.</p>
  </section>
</article>

As it's the holding block, let's give the article a fixed position:

article {
  position: fixed;
  outline: 1px solid blue;
  left: 1em;
  top: 3em;
}

Let's look at the visual candy. As in the previous example, we've added a transition to the opacity of each section. we've also added two more elements, setting a background image on each one. Since the sections are in a line we've altered the position of the background for each block so that the image tiles seamlessly.

section[id^="s"] {
width: 25em;
  height: 25em;
  position: absolute;
  background-image: url(271.jpg);
  background-size:25em 25em;  
  font-size: 1em;
  opacity: 0;
  transition: all 6s;
}

#s0 {
left: 0em;
  top: 0em;
  background-position: 0em 0em;
}

#s1 {
left: 25em;
  top: 0em;
  background-position: -25em 0em;
}

#s2 {
left: 50em;
  top: 0em;
  background-position: -50em 0em;
}

#s0:target, #s1:target, #s2:target {
  opacity: 1
}

You can test the first iteration here. But it isn't quite what we want, as in this version the links just make the sections appear in different parts of the screen; we want the effect to fill the screen and transition smoothly.

Moving blocks; the actual problem

Heading towards our desired interface, let's try to move the blocks. We need to add the transition declaration to each section:

#s0 {
left: 25em;
  top: 0em;
  background-position: 0em 0em;
  transition: all 3s;
}

#s1 {
left: 50em;
  top: 0em;
  background-position: -25em 0em;
  transition: all 3s;
}

#s2 {
left: 75em;
  top: 0em;
  background-position: -50em 0em;
  transition: all 3s;
}

And alter the rule for the targeted section accordingly, so that each section moves to a predetermined point.

#s0:target, #s1:target, #s2:target{
  opacity: 1;
  left: 0em;
  top: 0em;
}

this gives us iteration 2. But still, what I'm really looking for is a moving area inside a static viewport. For this we'll need to create a single viewport that scrolls to a specific location when an anchor is selected. So we have two options:

  1. When an anchor is selected, move every section into the corresponding positions, and show the relevant section.

    With three sections and three anchors, this would generate nine rules. With nine, it'd be eighty-one!

  2. When an anchor is selected, move the surrounding article to the correct position and only show the relevant section.

    Since there is only one article we only need to create one rule per anchor: so for three anchors that's three rules, and for nine, nine.

Taking the easier option, I'll choose the second option. To do this I'll have to change the type of positioning on the article from fixed to absolute. But we have no way of controlling the movement of the container! Our links point to the sections inside the article, so the result is the same as before.

You can't talk down to your elders and betters

Here is where we hit a limit of CSS. The C in CSS stands for cascading - and CSS rules are applied downwards or sideways in the same family, never upwards. So looking at the family relationships of our document structure:

  1. The body has two children: ul and article.
  2. Therefore, the article and ul are siblings.
  3. The article and ul each have three children.
  4. the children of the ul (the lis) have one child each (an a).

In terms of consanguinity (see this cousin tree diagram), the a is a grand-niece/grand-nephew of the article and thus cannot talk to the great-uncle/great-aunt. Additionally, in our stylesheet, we would be asking the children of the article to tell it what to do when each one was a target - which also does not work. So, to achieve the effect we want, we have to make some alterations to our document structure, and then to the stylesheet.

The solution

First of all, we know that we will be moving an absolutely positioned block around a fixed position element, so we have to contain our links and article inside another container, in this case a div:

<div>
  <ul id="nav">
    <li><a href="#s0">Welcome</a> </li>
    <li><a href="#s1">Tab styling</a></li>
    <li><a href="#s2">Applications</a></li>
  </ul>
  <article>
    <section id="s0">...</section>
    <section id="s1">...</section>
    <section id="s2">...</section>
  </article>
</div>

Let's adjust the style of the generic container so that it's fixed, and set the size of our viewport, which is the article element. We want the background image to fill the element completely. As we want to give the impression of movement, we apply the transition to this element.

We also need to remember that there is a stacking order to consider. Each subsequent element rendered on a page will stack above its predecessors; so a child sits on top of the parent, and the last sibling will sit above the first. If we want our exposed links to display correctly we'll need to add a z-index to the article. Giving it a negative number will stack it below the links.

div {
  position: fixed; 
  left:0em; top: 0em; 
}

div > article {
  width: 300em; height: 80em; 
  position:absolute; 
  left: 0em; top: 0em; 
  background-image:url(271.jpg); 
  background-size: 300em 80em; 
  background-repeat: no-repeat; 
  list-style-type: none; 
  font-size: 2em;
  z-index: -100;
  margin: 0; padding: 0;
  transition: top 1.5s 1.5s, left 1.5s 1s; 
}

Now, we need to have the anchors and the article at the same level, so we'll extract them from the list, leaving exposed links. Since the links are exposed, we'll need to separate them with something better than white space: so we'll surround our links in brackets, with each bracket surrounded by a span. However, we will keep our original list, for reasons that will become very clear later.


<span>{</span><a id="l0" href="#l0">Welcome
</a><span>}</span>
<span>{</span><a id="l1" href="#l1">Tab styling
</a><span>}</span> 
<span>{</span><a id="l2" href="#l2">Applications</a><span>}</span>

<ul id="nav">
  <li><a href="#s0">Welcome</a> </li>
  <li><a href="#s1">Tab styling</a></li>
  <li><a href="#s2">Applications</a></li>
</ul>

Since the child cannot affect a parent, the target declaration in our stylesheet also needs altering. As the exposed links are now siblings of the container, we can add meaningful fragment identifiers to each link, and have each one reference itself. So we'll use the heading of each section to generate the id and the href.

<span>{</span><a id="welcome" href="#welcome">Welcome
</a><span>}</span>
<span>{</span><a id="tabs" href="#tabs">Tab styling
</a><span>}</span> 
<span>{</span><a id="apps" href="#apps">Applications
</a><span>}</span>

In this case we've saved the job of working out the positions of each section in the block by floating them, and since we've stretched the article to be 300em × 80em, the three sections each must be 100em × 80em. This will have the effect of arranging the sections in a line.

article > section {
  display: block; 
  width: 100em;
  height: 80em;
  margin: 0em;
  padding: 0em; 
  float:left; 
  opacity: 0; 
  color: #250; 
  transition:all 1.5s;
}

We can now use the general sibling combinator (~) alongside :target to move the container...

#welcome:target ~ article {
  left: 0em;
  top: 0em;
} 

#tabs:target ~ article {
  left: -100em;
  top: 0em;
}

#apps:target ~ article {
  left: -200em;
  top: 0em;
}

...and modify the rule to display the appropriate sections. In this case we want the first section to display upon loading the page, so we set its initial opacity to 1, and then change it to zero when the other sections are targeted.

#tabs:target ~ article > section#s1 {
  opacity:1;
}

#apps:target ~ article > section#s2 {
  opacity:1;
}

section#s0 {
  opacity: 1;
}

#tabs:target ~ article > section#s0, #apps:target ~ article > section#s0 {
  opacity: 0;
}

We'll now add a bit of styling for the links. Here we're making them look like tabs that pop up slightly on a hover, and then extend fully on a target. We've used box-shadow to create the effect of a light source, and made them translucent. Finally, we've positioned them relative to our sections:

#tabs, #apps, #welcome {
  height: 1em; 
  position: relative; 
  left: 40em; top: -2em;  
  display: inline-block; 
  padding: 1ex; 
  border: 1px solid #230;
  border-bottom: none;
  width: auto;
  font-size: 1.4em;
  text-decoration: none;  
  border-radius: 2ex 2ex 0ex 0ex;
  box-shadow: -2px 1px 2px 0px rgba(157, 184, 122, 0.5) inset;
  background-color: rgba(127, 154, 102, 0.6); 
  transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s; 
}

#tabs:hover, #apps:hover, #welcome:hover {
  background-color: rgba(158, 192, 65, 0.6); 
  top: -3em; 
  height: 2em;
  box-shadow: -2px 1px 2px 0px rgba(187, 214, 152, 0.3) inset;
  transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s;
}

#tabs:target, #apps:target, #welcome:target {
  background-color: rgba(158, 192, 65, 1); 
  top: -4em;  
  height: 3em;
  box-shadow: -2px 1px 2px 0px rgba(217, 244, 182,  0.6) inset;
}

With some extra styling to the main page heading, we're almost there, but it will need tidying up — see the third iteration in action.

Saving graces

So what have we got left to do? Luckily, not much. We have used a lot of CSS that legacy browsers will not understand and thus will be ignored: for example, Internet Explorer 6, 7 and 8 will get a single page with no positioning, but all content will still be viewable. The only things we need to do to tidy up are:

  1. ensure that the HTML5 elements are displayed as blocks

    This is simple:

    section, article {display: block;}
  2. remove the exposed links below the main page title; but show them for more modern browsers

    We can simply use the cascade for this, adding a display: none rule above our declarations for the links: we can also hide the span elements with this rule.

    #tabs, #apps, #welcome, span {
      display: none;
    }
    
    #tabs, #apps, #welcome {
      height: 1em; 
      position: relative; 
      left: 40em;
      top: -2em;  
      display: inline-block; 
      padding: 1ex; 
      border: 1px solid #230; border-bottom: none;
      width: auto;
      font-size: 1.4em; text-decoration: none;  
      border-radius: 2ex 2ex 0ex 0ex;
      box-shadow: -2px 1px 2px 0px rgba(157, 184, 122, 0.5) inset;
      background-color: rgba(127, 154, 102, 0.6); 
      transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s; 
    }

    In older browsers, the more advanced styles will be skipped over.

  3. hide the original navigation list from more modern browsers

    Note that we cannot get rid of the list, since our exposed links are self-referencing and thus wouldn't take the user to the right section. We want to make sure the page is still usable by text browsers such as Lynx and W3M, as well as screen-readers. So we can move our list so that it comes after the article: this will also help us position the exposed links in the right place.

    
    <article>
      <section id="s2">
        <h2>Applications</h2>
        <p>...</p>
      </section>
    </article>
    
    <ul id="nav">
      <li><a href="#s0">Welcome</a></li>
      <li><a href="#s1">Tab styling</a></li>
      <li><a href="#s2">Applications</a></li>
    </ul>

    Then for our exposed links, we can include a heading and link, writing a short sentence to explain the situation to text and screen-readers:

    <h2 id="skip"><span>non-functioning links for graphical browser users</span> [<a href="#nav">skip to menu</a>]</h2>

    Now we can use a media query to hide these things from more modern browsers.

    @media all and (min-width: 1px) {
      #nav {
        display: none;
      }
      
      #skip {
        display: none;
      }
    }

    This gives a reasonable layout on a text browser, and makes the menu accessible on legacy graphical browsers.

    Note: There is of course a catch. W3M does not understand some of the new elements of HTML5 - so the links pointing to a section will not work. To rectify this you'll have to use a div instead.

And We now have an interface that scrolls smoothly from section to section, and which degrades gracefully on older browsers to a single page - try out the fourth iteration.

And yet we still have some...

Issues with spiders and reptiles

Or: what are are we going to do about Gecko and Webkit?

As we mentioned briefly in the last article and in slightly more detail in its discussion, Webkit has a bug that means it can't deal with this standards-compliant technique: this results in it not honouring the general sibling combinator in conjunction with a pseudo selector. Since we know that our technique won't work with Webkit browsers, we have to hide our styling from Webkit by wrapping our rules inside a media query to one that is Webkit-specific, using the -webkit- prefix. Here, we extract our rules from our original media query, place them and pretty much everything else in a new one, and remove all the -webkit- prefixed styles. The full media query will look like so:

@media not all and (-webkit-min-device-pixel-ratio: 0) {

  #nav {
    display: none;
  }

  #skip {
    display: none;
  }

  #tabs, #apps, #welcome {
    height: 1em; 
    display: inline-block;
    position: relative; 
    left: 40em;
    top: -2em;
    padding: 1ex; 
    border: 1px solid #230; 
    border-bottom: none; 
    width: auto;
    font-size: 1.4em; 
    text-decoration: none;  
    background-color: rgba(127, 154, 102, 0.6); 

    border-radius: 2ex 2ex 0ex 0ex;       /* Opera understands this as-is */
    -moz-border-radius: 2ex 2ex 0ex 0ex;

    box-shadow: -2px 1px 2px 0px rgba(157, 184, 122, 0.5) inset;    /* Opera also understands this */
    -moz-box-shadow: -2px 1px 2px 0px rgba(157, 184, 122, 0.5) inset;

    -o-transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s;
    -moz-transition: -moz-box-shadow 2.5s, background-color 2s, top 1s, height 1s;
    transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s; 
  }

  #tabs:hover, #apps:hover, #welcome:hover {
    background-color: rgba(158, 192, 65, 0.6); 
    top: -3em; 
    height: 2em;

    box-shadow: -2px 1px 2px 0px rgba(187, 214, 152,  0.3) inset;
    -moz-box-shadow: -2px 1px 2px 0px rgba(187, 214, 152,  0.3) inset;

    -o-transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s;
    -moz-transition: -moz-box-shadow 2.5s, background-color 2s, top 1s, height 1s;
    transition: box-shadow 2.5s, background-color 2s, top 1s, height 1s;
  }

  #tabs:target, #apps:target, #welcome:target {
    background-color: rgba(158, 192, 65, 1); 
    top: -4em;  
    height: 3em;
    -moz-box-shadow: -2px 1px 2px 0px rgba(217, 244, 182,  0.6) inset;
    box-shadow: -2px 1px 2px 0px rgba(217, 244, 182,  0.6) inset;
  }

  div > h1 { 
    width: 80em; 
    padding: 1.5ex; 
    background-color: rgba(10, 10, 10, 0.8); 
    color: #efe; 
    font-size: 2.5em; 
    margin: 0em;
  }

  div > h2 {
    display: inline;
  }

  div {
    position: fixed; 
    left:0em; top: 0em; 
  }

  div > article {
    width: 300em; height: 80em; 
    position:absolute; 
    left: 0em;
    top: 0em; 
    background-image:url(271.jpg); 
    background-size: 300em 80em; 
    -moz-background-size: 300em 80em; 
    background-repeat: no-repeat; 
    z-index: -100; 
    list-style-type: none; 
    font-size: 2em;
    margin: 0;
    padding: 0;
    -o-transition: top 1.5s 1.5s, left 1.5s 1s; 
    -moz-transition: top 1.5s 1.5s, left 1.5s 1s; 
    transition: top 1.5s 1.5s, left 1.5s 1s; 
  }

  article > section {
    width: 100em;
    height: 80em;
    margin: 0em;
    padding: 0em; 
    float:left; 
    opacity: 0; 
    color: #250; 
    -o-transition: all 1.5s; 
    -moz-transition: all 1.5s;
    transition: all 1.5s; 
  }

  section[id^="s"] > p {
    width: 36em;
    padding-left: 2em;
    font-size: 0.7em;
  }

  section[id^="s"] > h2, section[id^="s"] > h3 {
    padding-left: 1em;
    margin-bottom: 0.1em;
    margin-top: 3em;
  }

  #tabs:target ~ article > section#s1 {
    opacity: 1;
  }
  
  #apps:target ~ article > section#s2 {
    opacity: 1;
  }

  section#s0 {
    opacity: 1;
  }

  #tabs:target ~ article > section#s0, #apps:target ~ article > section#s0 {
    opacity: 0;
  }

  #welcome:target ~ article {
    left: 0em;
    top: 0em;
  } 

  #tabs:target ~ article {
    left: -100em;
    top: 0em;
  }

  #apps:target ~ article {
    left: -200em;
    top: 0em;
  } 
}

Try the fifth iteration, with Webkit media query.

In this case, we're saying that the styles should not be applied to those screens that have a Webkit-specific device pixel ratio of zero or more.

Opera drops all of a declaration if it doesn't understand part of it, so it essentially sees the same thing as the CSS validator:

@media

and thus Opera applies the styles inside the media query. However, Firefox drops the whole declaration as (according to its debugger) it actually sees this:

@media not all

So, frustratingly, it does not apply the styles inside the media query. So by "fixing" the page for Webkit, the styling is lost for Gecko.

Note: This really isn't ideal, as it is effectively Webkit-specific browser sniffing, and it causes the layout to break in Webkit and Gecko. I am just trying to demonstrate a way to get Webkit to read the page successfully. The best approach would be to lobby Webkit to fix this bug!

Conclusion

We've worked our way through various applications of :target - from the fairly simple to more complex and visually impressive user interfaces without heavy scripting. A bit of thought is required, but through careful use of transitions, media queries and pseudo selectors, we can create functional sites with interesting new designs.

code

Corey Mwamba lives in England and makes joyful sounds for a living. But when he's not doing that he can be found at his computer adding very basic touches to his ten-year-old standards-based site, or writing nonsense on his Opera blog.

Corey owns no pets, but would dearly like a tortoise.


This article is licensed under a Creative Commons Attribution 3.0 Unported license.

Comments

The forum archive of this article is still available on My Opera.

No new comments accepted.