CSS Diagrams or Things to Do With CSS When You're Bored at Work or Look Ma’ – No Images

Background

I encountered a problem at work one day: I needed to create a set of state transition diagrams for the documentation of a simple XML parser I had written. The documentation is done in HTML, so my first idea was to draw the diagrams as GIF or PNG images. Unfortunately, the only drawing tool available was Microsoft Paint – in a version that would only save files in BMP format.

Now, there are no problems – only opportunities. However, this seemed to be an insurmountable opportunity. I wanted the diagrams to contain links to other diagrams, and since the rest of the text was in a fluid layout where the user could alter the text size, it would be nice to have the same features in the diagrams. But you can't draw diagrams with HTML and CSS, right? I mean, right?

Actually, you can …

An Example

The example below is the state transition diagram for the initial state of my parser. The links don't lead anywhere, so don't bother clicking on them. Do change the text size in your browser, though, and look at the example with styles turned off if your browser lets you.

State Diagram

Start

HTML

OpenRootTag

OpenRootDirective

How Does It Work?

The whole diagram is wrapped in a <div> which has its id attribute specified as diagram. (If you expect to have more than one diagram in a page, use class instead.) The diagram has the following style sheet rule (irrelevant declarations are omitted for brevity):

#diagram {
  position: relative;
  height:   24em;
}

position:relative is very important. The children of this <div> are absolutely positioned. Absolutely positioned elements are positioned relative to the nearest positioned ancestor. Since we want them to be positioned relative to the diagram itself, we have to make it positioned. By setting position:relative without specifying an offset, it will become positioned without being moved.

The height declaration is just to reserve the necessary vertical space, so that any text following the diagram is pushed down. Since all children of the diagram are positioned, it would otherwise collapse entirely.

Now, let's look at how a state is specified:

<div class="row3 col1 size1">
  <h3>HTML</h3>
  <ul>
    <li class="circ-right"><samp>*</samp><span> (any charater) is allowed.</span></li>
    <li class="down height1"><samp>&lt;</samp><span> causes a transition to
    the </span><a href="#" class="size3">OpenContentTag</a><span> state.</span></li>
  </ul>
</div>

This <div>'s appearance is determined by the following style sheet rules:

#diagram div {
  position: absolute;
  border:   0.05em solid #000;
}

.row3  { top:   15em }
.col1  { left:  3em }
.size1 { width: 4em }

The first rule establishes that the state boxes are absolutely positioned (relative to the diagram, as we made sure of above), and that they have a solid black border. Why use ems for the border width? What's wrong with the good ol' pixel? That has to do with the infamous box model of CSS, where any padding and border width is added to the specified width and height of an element. Since we will need very precise locations for our arrows later on, we'll have to take the border width into consideration. We'll want to use ems for the width of the boxes, to accommodate changes in text size, so we're stuck with the same unit for the borders as well. The value 0.05em all but ensures that the border will be one pixel wide, unless the user has a really large font – like 30 pixels (in which case he'll probably need a two-pixel border to see it anyway).

The three classes specify the position and size of the box. You could just use style attributes, of course, but this provides better separation between content and style, and also makes it easier to move boxes around.

The first element inside the state box is:

<h3>HTML</h3>

The style sheet will center the text and increase the line height, to give some vertical space above and below the state name. (You'll have to use padding if your text can span multiple rows.)

The transitions (arrows) are specified in an unordered list, from which all margins and padding has been removed, and whose list style is set to none.

Each list item specifies one transition. Let's look at the first one:

<li class="circ-right">...</li>

A number of style sheet rules affect this one:

#diagram li {
  position: absolute;
  margin:   0;
  padding:  0;
}

#diagram .circ-right {
  top:         0.5em;
  left:        100%;
  margin-left: 0.05em;
  width:       1em;
  height:      1em;
  border:      0.05em solid #000;
  border-left: 0;
}

The first rule specifies that all list items that are descendants of the diagram <div> are absolutely positioned, just as the state boxes. It will also remove any margins and padding that browsers may be tempted to add to them.

The second rule defines the particulars for the circ-right class. It has a thin, black border on three sides. It is one em square. The top:0.5em declaration just pushes it down a bit. left:100% means that the "box" containing the list item will appear just to the right of the parent element (the state box <div>). But that's where the border is, so we add margin-left:0.05em to clear that.

But what about the arrowhead? We'll soon come to that. Let's look at the content of the list item. There are two inline elements inside the <li>. The first is simple:

<samp>*</samp>

This is the "symbol" that causes the transition, and we want it to appear next to the transition itself. The style sheet defines:

#diagram .circ-right samp {
  position: absolute;
  left:     100%;
}

In other words: put the sample text to the right of the list item's box.

The second child of the <li> is a <span>, but it doesn't appear anywhere in the diagram! Style sheet:

#diagram li span {
  position: absolute;
  width:    0;
  height:   0;
  overflow: hidden;
}

OK, setting the size to nothing and using overflow:hidden will make the text disappear. Hopefully, a screen reader would still read it, and a non-CSS browser will display it as if the <span> tags weren't there. But what's with the position:absolute? Well, there's another rule in the style sheet:

#diagram .circ-right span {
  top:           -0.3em;
  left:          0;
  border-top:    0.3em solid #fff;
  border-right:  0.5em solid #000;
  border-bottom: 0.3em solid #fff;
}

What?! First you say that it should be invisible, then you start positioning it and drawing fat borders? And where's the left border? Are you on drugs?

Remember that arrowhead we touched on lightly a minute ago? Well, here it is. The trick of using three thick borders, two of which in the background color, to make triangles is nothing new. And what's an arrowhead, if not a triangle? By carefully positioning it (relative to its parent; the <li>), we can make it appear as an arrowhead on our "arrow".

IE5.x/Win won't play fair, though. It will insist on making the element box one em high, no matter what. That will put the arrowhead in the wrong place. I've found a reasonable fix: set the font size to zero, and use pixels for the arrowhead borders. It won't scale, but it will draw the arrowheads.

The second item in the list looks similar:

<li class="down height1"><samp>&lt;</samp><span> causes a transition to
  the </span><a href="#" class="size3">OpenContentTag</a><span> state.</span></li>

The list item has another transition class, down, which defines a straight line going down from the bottom of the state box.

#diagram .down {
  top:         100%;
  left:        50%;
  margin-top:  0.05em;
  border-left: 0.05em solid #000;
}

top:100% will push it below the state box, and margin-top:0.05em will clear the box's border. left:50% will put the left edge of the list item at the horizontal center of the state box. Finally, the border-left declaration draws the arrow line. The height1 class specified for the list item controls the length of the line. Then the arrowhead, defined for the <span> again:

#diagram .down span {
  bottom:       0;
  left:         -0.3em;
  border-top:   0.5em solid #000;
  border-right: 0.3em solid #fff;
  border-left:  0.3em solid #fff;
}

Similar to the one above, but using other borders and a slightly different positioning.

But wait a minute. There are two <span>s in that list item! Yes, and they'll both draw an identical arrowhead in the exact same place. If that offends your sense of aesthetics, feel free to define a class, or find a better way of distinguishing them. It would be so easy to do this with an adjacent selector (#diagram .down samp+span), but a certain frequently used browser from a major corporation doesn't understand it.

But why are there two <span>s in the first place? That's because of the <a> in the middle. That defines a "complex" state, which is supposedly defined in a separate state diagram. It is displayed as a light gray box with a dotted border, thanks to the following style sheet rule:

#diagram div li a {
  display:          block;
  position:         absolute;
  border:           0.05em dotted #666;
  background-color: #eee;
  line-height:      2;
  text-align:       center;
}

Plus a few rules that specify the text color for the various pseudo-classes. The placement of those gray boxes are determined by the class of the transition (list item) that contain them. In this case:

#diagram .down a {
  top:  100%;
  left: 0;
}

In other words, at the bottom of the line, left-aligned. But that doesn't look good; we'd want the arrow to hit the gray box at its horizontal center. So we need another rule:

a.size3 { margin-left: -5em }

(The size3 class is defined to be 10em wide, and is used for the size of the gray box. A negative margin equal to half the width will accomplish what we're looking for.)

OK, but that still doesn't explain why the <a> isn't inside the <span>. Unfortunately, it can't be. We need overflow:hidden on the <span> to hide the text, and that will cause the <a> to be hidden as well, no matter what we do. So we'll have to break up the <span> and have the <a> as an immediate child of the <li> instead.

The rest is no different. The class of the list item determine where they are positioned in relation to the state box, and which borders are visible. There are a few cases that aren't covered in this demo (classes like left, down-left, etc.), but those are trivial. They are left as an exercise for the reader, as it used to say in my old college text books.

Conclusions

If you find this interesting or useful, just look at the document's source code and feel free to use it as you please. The CSS is all there in the <head>. That's just for this sample file, of course. In reality, it would go in an external style sheet.

I think that this will degrade reasonably gracefully in a non-CSS browser. It looks fair enough without styles.

The only drawback as I see it is that the markup code is a bit bloated. (Apart from the fact that I'm using a markup language and a style sheet language to accomplish something that ought to be done with SVG, of course.) I've tried to keep the number of classes to a minimum.

The CSS isn't optimized for performance, but for readability. Hold the hate mail, please.

Oh, and of course this validates as HTML 4.01 Strict and CSS 2. (It would be just as easy to make it validate as XHTML 1.1, but I can't be bothered to write a PHP script to do the content negotiation just for this.)

Browser Bugs

I have tested this with the following browsers under Windows XP:

It works the same in all three browsers. Of course, IE5.x (Windows) causes problems, but I didn't have access to that one at the time of writing this. I couldn't make it work with the relatively sized arrowheads in that one, when I first tried this at work. IE5.5 insisted on making the <div> one em high, even if it was empty. That made the arrows appear too high, and since there is no border-color:transparent, they would obscure anything underneath. The only solution I could come up with was to set the font size to zero and use pixels for the arrowheads. I haven't tried it properly with this new and improved version (just IE6 in quirks mode), but I included that hack just in case.

I've noticed that Mozilla may lose some borders on the state boxes if you scroll down past the diagram and then scroll back up again (slowly). A refresh will take care of that, though. Since I've used auto for the horizontal margins, Mozilla will dutifully center the diagram even if the window is too narrow for it to fit. The result is that the left-most part of the diagram is inaccessible. That has nothing to do with the diagram-drawing technique itself, though.

I know nothing about Mac browsers, and I haven't had a chance yet to try it under Linux. If you do, I'd appreciate any information you can give me.

Update: Michael was kind enough to send me a screenshot of the results in Safari 1.1 on Mac OS X. It appears as if the borders and "arrow" lines aren't displayed. My guess is that it computes 0.05em to something less than 1px and rounds downward to zero. You could use pixels for the borders, if you don't mind a slight overlap at one end of the "arrows".

Thoughts?

If you'd like to comment on this, please send me an e-mail. I won't include a real mailto link, because I'm so tired of spam, but I'm sure you'll be able to figure it out anyway, especially if you're using Opera7 …

E-mail: tool-man AT home DOT se