CSS3 :target based interfaces
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.
- Background information
- A look at the example code
- Explanations
- Issues with spiders and reptiles
- 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 section
s 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:
When an anchor is selected, move every
section
into the corresponding positions, and show the relevantsection
.With three sections and three anchors, this would generate nine rules. With nine, it'd be eighty-one!
When an anchor is selected, move the surrounding
article
to the correct position and only show the relevantsection
.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:
- The
body
has two children:ul
andarticle
. - Therefore, the
article
andul
are siblings. - The
article
andul
each have three children. - the children of the
ul
(theli
s) have one child each (ana
).
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 section
s 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 section
s. 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:
- ensure that the HTML5 elements are displayed as blocks
This is simple:
section, article {display: block;}
- 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 thespan
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.
- 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 adiv
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.
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.