Section 2.5. Using Document Structure

2.5. Using Document Structure

As I've mentioned before, CSS is powerful because it uses the structure of HTML documents to determine appropriate styles and how to apply them. That's only part of the story since it implies that such determinations are the only way CSS uses document structure. Structure plays a much larger role in the way styles are applied to a document. Let's take a moment to discuss structure before moving on to more powerful forms of selection.

2.5.1. Understanding the Parent-Child Relationship

To understand the relationship between selectors and documents, you need to once again examine how documents are structured. Consider this very simple HTML document:

Much of the power of CSS is based on the parent-child relationship of elements. HTML documents (actually, most structured documents of any kind) are based on a hierarchy of elements, which is visible in the "tree" view of the document (see Figure 2-14). In this hierarchy, each element fits somewhere into the overall structure of the document. Every element in the document is either the parent or the child of another element, and it's often both.

Figure 2-14. A document tree structure

An element is said to be the parent of another element if it appears directly above that element in the document hierarchy. For example, in Figure 2-14, the first p element is parent to the em and strong elements, while strong is parent to an anchor element, which is itself parent to another em element. Conversely, an element is the child of another element if it is directly beneath the other element. Thus, the anchor element in Figure 2-14 is a child of the strong element, which is in turn child to the p element, and so on.

The terms parent and child are specific applications of the terms ancestor and descendant. There is a difference between them: in the tree view, if an element is exactly one level above another, then they have a parent-child relationship. If the path from one element to another continues through two or more levels, the elements have an ancestor-descendant relationship, but not a parent-child relationship. (Of course, a child is also a descendant, and a parent is an ancestor.) In Figure 2-14, the first ul element is parent to two li elements, but the first ul is also the ancestor of every element descended from its li element, all the way down to the most deeply nested li elements.

Also, in Figure 2-14, there is an anchor that is a child of strong, but also a descendant of paragraph, body, and html elements. The body element is an ancestor of everything that the browser will display by default, and the html element is ancestor to the entire document. For this reason, the html element is also called the root element.

2.5.2. Descendant Selectors

The first benefit of understanding this model is the ability to define descendant selectors (also known as contextual selectors). Defining descendant selectors is the act of creating rules that operate in certain structural circumstances but not others. As an example, let's say you want to style only those em elements that are descended from h1 elements. You could put a class attribute on every em element found within an h1, but that's almost as time-consuming as using the font tag. It's obviously far more efficient to declare rules that match only em elements that are found inside h1 elements.

To do so, write the following:

h1 em {color: gray;}

This rule will make gray any text in an em element that is the descendant of an h1 element. Other em text, such as that found in a paragraph or a block quote, will not be selected by this rule. Figure 2-15 makes this clear.

Figure 2-15. Selecting an element based on its context

In a descendant selector, the selector side of a rule is composed of two or more space-separated selectors. The space between the selectors is an example of a combinator. Each space combinator can be translated as "found within," "which is part of," or "that is a descendant of," but only if you read the selector right to left. Thus, H1 em can be translated as, "Any em element that is a descendant of an h1 element." (To read the selector left to right, you might phrase it something like, "Any h1 that contains an em will have the following styles applied to the em.")

You aren't limited to two selectors, of course. For example:

ul ol ul em {color: gray;}

In this case, as Figure 2-16 shows, any emphasized text that is part of an unordered list that is part of an ordered list that is itself part of an unordered list (yes, this is correct) will be gray. This is obviously a very specific selection criterion.

Figure 2-16. A very specific descendant selector

Descendant selectors can be extremely powerful. They make possible what could never be done in HTMLat least not without oodles of font tags. Let's consider a common example. Assume you have a document with a sidebar and a main area. The sidebar has a blue background, the main area has a white background, and both areas include lists of links. You can't set all links to be blue because they'd be impossible to read in the sidebar.

The solution: descendant selectors. In this case, you give the table cell that contains your sidebar a class of sidebar, and assign the main area a class of main. Then, you write styles like this:

Figure 2-17. Using descendant selectors to apply different styles to the same type of element

:link refers to links to resources that haven't been visited. We'll talk about it in detail later in this chapter.

Here's another example: let's say that you want gray to be the text color of any b (boldface) element that is part of a blockquote, and also for any bold text that is found in a normal paragraph:

blockquote b, p b {color: gray;}

The result is that the text within b elements that are descended from paragraphs or block quotes will be gray.

One overlooked aspect of descendant selectors is that the degree of separation between two elements can be practically infinite. For example, if you write ul em, that syntax will select any em element descended from a ul element, no matter how deeply nested the em may be. Thus, ul em would select the em element in the following markup:

2.5.3. Selecting Children

In some cases, you don't want to select an arbitrarily descended element; rather, you want to narrow your range to select an element that is a child of another element. You might, for example, want to select a strong element only if it is a child (as opposed to a descendant) of an h1 element. To do this, you use the child combinator, which is the greater-than symbol (>):

h1 > strong {color: red;}

This rule will make red the strong element shown in the first H1 below, but not the second:

Read right to left, the selector H1 > strong translates as "selects any strong element that is a child of an h1 element." The child combinator is optionally surrounded by whitespace. Thus, H1 > strong, h1> strong, and h1>strong are all equivalent. You can use or omit whitespace as you wish.

When viewing the document as a tree structure, it's easy to see that a child selector restricts its matches to elements that are directly connected in the tree. Figure 2-18 shows part of a document tree.

Figure 2-18. A document tree fragment

In this tree fragment, you can easily pick out parent-child relationships. For example, the a element is parent to the strong, but it is child to the p element. You could match elements in this fragment with the selectors p > a and a > strong, but not p > strong, since the strong is a descendant of the p but not its child.

You can also combine descendant and child combinations in the same selector. Thus, table.summary td > p will select any p element that is a child of a TD element that is itself descended from a table element that has a class attribute containing the word summary.

2.5.4. Selecting Adjacent Sibling Elements

Let's say you want to style the paragraph immediately after a heading or give a special margin to a list that immediately follows a paragraph. To select an element that immediately follows another element with the same parent, you use the adjacent-sibling combinator, represented as a plus symbol (+). As with the child combinator, the symbol can be surrounded by whitespace at the author's discretion.

To remove the top margin from a paragraph immediately following an H1 element, write:

h1 + p {margin-top: 0;}

The selector is read as, "selects any paragraph that immediately follows an h1 element that shares a parent with the p element."

To visualize how this selector works, it is easiest to once again consider a fragment of a document tree, shown in Figure 2-19.

Figure 2-19. Another document tree fragment

In this fragment, a pair of lists descends from a div element, one ordered and the other not, each containing three list items. Each list is an adjacent sibling, and the list items themselves are also adjacent siblings. However, the list items from the first list are not siblings of the second, since the two sets of list items do not share the same parent element. (At best, they're cousins.)

Remember that you can select the second of two adjacent siblings only with a single combinator. Thus, if you write li + li {font-weight: bold;}, only the second and third items in each list will be boldfaced. The first list items will be unaffected, as illustrated in Figure 2-20.

Figure 2-20. Selecting adjacent siblings

To work properly, CSS requires that the two elements appear in "source order." In our example, an ol element is followed by a ul element. This allows you to select the second element with ol + ul, but you cannot select the first using the same syntax. For ul + ol to match, an ordered list must immediately follow an unordered list.

In addition, text content between two elements does not prevent the adjacent-sibling combinator from working. Consider this markup fragment, whose tree view would be the same as that shown in Figure 2-19:

Even though there is text between the two lists, you can still match the second list with the selector ol + ul. That's because the intervening text is not contained with a sibling element, but is instead part of the parent div. If you wrapped that text in a paragraph element, it would then prevent ol + ul from matching the second list. Instead, you might have to write something like ol + p + ul.

As the following example illustrates, the adjacent-sibling combinator can be used in conjunction with other combinators:

html > body table + ul{margin-top: 1.5em;}

The selector translates as "selects any ul element that immediately follows a sibling table element that is descended from a body element that is itself a child of an html element."

Internet Explorer for Windows through IE6 does not support child and adjacent-sibling selectors. IE7 supports both.