Arm yourself with Drupal $body_classes!

A common question I get for theming Drupal sites is how to apply css to specific pages. For example, you might want your background image for your header to change on specific pages. Out of the box, Drupal doesn’t give you any class in the HTML that’s unique to your node to grab onto. This is where $body_classes come in.

Drupal 6 introduced the $body_classes variable to dynamically include certain information about the page you are on as a class of your body tag. These include:

– .no-sidebar/.one-sidebar/.two-sidebars
– .sidebar-left/.sidebar-right
– .front/.not-front
– .logged-in/.not-logged-in
– .page-[PAGE TYPE]
– .node-type-[CONTENT TYPE]

Some starter themes, like Zen, come with the $body_classes variable already in place. If you are building your theme from scratch or using a theme that doesn’t have it, you’ll need to add it in.

To add $body_classes to your theme, open your page.tpl.php file, and find your body tag. Edit it to be: <body class="<?php print $body_classes ?>">. Now, when you check your HTML, your body tag should look something like this:

<body class="not-front logged-in page-node node-type-forum one-sidebar sidebar-right">

You can probably see how these will give someone building a theme good stuff to work with. However, sometimes this isn’t enough; sometimes you need node-specific classes. With Drupal 6, you can use a preprocess_page function to add on to your $body_classes variable.

First, you will need to create a function that properly takes your URL and converts it to a class (you don’t want any space, capitals or irregular characters). Open your template.php file and add the following function:


<?php
/**
* Converts a string to a suitable html ID attribute.
*
* http://www.w3.org/TR/html4/struct/global.html#h-7.5.2 specifies what makes a
* valid ID attribute in HTML. This function:
*
* - Ensure an ID starts with an alpha character by optionally adding an 'id'.
* - Replaces any character except A-Z, numbers, and underscores with dashes.
* - Converts entire string to lowercase.
*
* @param $string
*   The string
* @return
*   The converted string
*/
function YOURTHEME_id_safe($string) {
  // Replace with dashes anything that isn't A-Z, numbers, dashes, or underscores.
  $string = strtolower(preg_replace('/[^a-zA-Z0-9_-]+/', '-', $string));
  // If the first character is not a-z, add 'n' in front.
  if (!ctype_lower($string{0})) { // Don't use ctype_alpha since its locale aware.
    $string = 'id' . $string;
  }
  return $string;
}
?>

This is reusable code – use it anywhere you need to convert a string into an id.

Next, search for a section that starts with YOURTHEME_preprocess_page. If you don’t have one, just add in this code anywhere in the file:

<?php
/**
* Intercept page template variables
*
* @param $vars
*   A sequential array of variables passed to the theme function.
*/
function YOURTHEME_preprocess_page(&$vars) {
  if (!$vars['is_front']) {
    // Add unique classes for each page and website section
    $path = drupal_get_path_alias($_GET['q']);
    list($section, ) = explode('/', $path, 2);
    $vars['body_classes'] .= ' ' . YOURTHEME_id_safe('page-' . $path) . ' ';
    $vars['body_classes'] .= ' ' . YOURTHEME_id_safe('section-' . $section) . ' ';
  }
}
?>

Otherwise, you will need to merge this code into the preprocess_page function.

This code gives you two new classes: page-PATH and section-SECTION. It might look something like:<body class="not-front logged-in page-node node-type-forum one-sidebar sidebar-right page-about-history section-about">. The page-PATH class displays the full url path, and the section is just the first arg in the path – enabling you to do some section-based theming, if your URLs are all segmented properly.

The final trick is to add your taxonomy terms to $body_classes. After the new section in your preprocess_page function where you turn urls into your $body_classes, add this snippet:

<?php
if (isset($vars['node']->taxonomy)) {
  // Add unique classes based on taxonomy terms
  $taxonomy_classes = array();
  foreach($vars['node']->taxonomy as $term_info) {
    $taxonomy_classes[] = 'taxonomy-'.YOURTHEME_id_safe($term_info->name);
  }
  $vars['body_classes'] .= " ".implode(' ', $taxonomy_classes); // Concatenate with spaces
}
?>

This will add a class to your body tag that looks like taxonomy-YOURTAG. All together, your preprocess_page function might look like this:

<?php
/**
* Intercept page template variables
*
* @param $vars
*   A sequential array of variables passed to the theme function.
*/
 function YOURTHEME_preprocess_page(&$vars) {
if (!$vars['is_front']) {
     // Add unique classes for each page and website section
    $path = drupal_get_path_alias($_GET['q']);
    list($section, ) = explode('/', $path, 2);
    $vars['body_classes'] .= ' ' . YOURTHEME_id_safe('page-' . $path) . ' ';
    $vars['body_classes'] .= ' ' . YOURTHEME_id_safe('section-' . $section) . ' ';
  }
  if (isset($vars['node']->taxonomy)) {
    // Add unique classes based on taxonomy terms
    $taxonomy_classes = array();
    foreach($vars['node']->taxonomy as $term_info) {
      $taxonomy_classes[] = 'taxonomy-'.YOURTHEME_id_safe($term_info->name);
    }
    $vars['body_classes'] .= " ".implode(' ', $taxonomy_classes); // Concatenate with spaces
  }
}
?>

Armed with those $body_classes, there is very little page- and section-specific CSS targeting you can’t do!