Adding CSS Classes to Fields in Drupal

Implementing any type of modular CSS requires fine grain control over classes in your HTML. In my last post, we looked at how we can easily add classes to blocks in Drupal using preprocess functions. Now let's apply the same technique to fields. Fields are a fundamental building block of any Drupal 7 site. Fields contain content and content is king. Before we dig in and start classing up our fields, let's look at how they are rendered to the screen.

Template Files and Theme Functions

All* HTML that Drupal sends to the browser is rendered using one of two methods, a theme function or template file. Which one is determined by the module invoking hook_theme(). If you can't find a template file for the HTML you want to change, chances are it's being created by a theme function. While the two look different on the surface, they both do essentially the same thing. They take either a $variables array or render $element and return a string of, more often than not, HTML. Themes override both template files and theme functions in a similar fashion. You copy the existing function or template from a module into your theme and edit to your heart's desire.
*While this should be true, there are plenty of instances where a developer has found some creative way to produce markup that makes it less accesible to the theme. If you run into this, you should file a bug report in the module's issue queue—or better yet, patch it!
Last time, we had to copy the core block.tpl.php template file to our theme and make some edits in order to take complete control of our block classes. We need to do this for fields as well. By default, fields are rendered by a theme function called theme_field() rather than a template file. So in order to modify the core function, we need to copy theme_field() into our theme's template.php file and rename it to mytheme_field. Where mytheme is the name of our theme. Below is an example of what your new theme function should look like.

<br />
/**<br />
 * Overrides theme_field()<br />
 * Remove the hard coded classes so we can add them in preprocess functions.<br />
 */</p>
<p>function mytheme_field($variables) {<br />
  $output = '';</p>
<p>  // Render the label, if it's not hidden.<br />
  if (!$variables['label_hidden']) {<br />
    $output .= '<div ' . $variables['title_attributes'] . '>' . $variables['label'] . ': </div>';<br />
  }</p>
<p>  // Render the items.<br />
  $output .= '<div ' . $variables['content_attributes'] . '>';<br />
  foreach ($variables['items'] as $delta => $item) {<br />
    $output .= '<div ' . $variables['item_attributes'][$delta] . '>' . drupal_render($item) . '</div>';<br />
  }<br />
  $output .= '</div>';</p>
<p>  // Render the top-level DIV.<br />
  $output = '<div class="' . $variables['classes'] . '"' . $variables['attributes'] . '>' . $output . '</div>';</p>
<p>  return $output;<br />
}<br />

You'll notice this looks just like the core theme_field() but with two differences. We removed the hardcoded class attributes from the label and content wrapper divs. This will allow us to manage these classes with a preprocess function on a per field basis. Just like template files, you can alter the variables that get passed to theme functions. Let's take a look at our preprocess function and see how we're adding classes.
<br />
/**<br />
 * Implements hook_preprocess_field()<br />
 <em>/</p>
<p>function mytheme_preprocess_field(&$vars) {<br />
  /</em> Set shortcut variables. Hooray for less typing! <em>/<br />
  $name = $vars['element']['#field_name'];<br />
  $bundle = $vars['element']['#bundle'];<br />
  $mode = $vars['element']['#view_mode'];<br />
  $classes = &$vars['classes_array'];<br />
  $title_classes = &$vars['title_attributes_array']['class'];<br />
  $content_classes = &$vars['content_attributes_array']['class'];<br />
  $item_classes = array();</p>
<p>  /</em> Global field classes <em>/<br />
  $classes[] = 'field-wrapper';<br />
  $title_classes[] = 'field-label';<br />
  $content_classes[] = 'field-items';<br />
  $item_classes[] = 'field-item';</p>
<p>  /</em> Uncomment the lines below to see variables you can use to target a field <em>/<br />
  // print '<strong>Name:</strong> ' . $name . '<br/>';<br />
  // print '<strong>Bundle:</strong> ' . $bundle  . '<br/>';<br />
  // print '<strong>Mode:</strong> ' . $mode .'<br/>';</p>
<p>  /</em> Add specific classes to targeted fields <em>/<br />
  switch ($mode) {<br />
    /</em> All teasers <em>/<br />
    case 'teaser':<br />
      switch ($name) {<br />
        /</em> Teaser read more links <em>/<br />
        case 'node_link':<br />
          $item_classes[] = 'more-link';<br />
          break;<br />
        /</em> Teaser descriptions */<br />
        case 'body':<br />
        case 'field_description':<br />
          $item_classes[] = 'description';<br />
          break;<br />
      }<br />
      break;<br />
  }</p>
<p>  switch ($name) {<br />
    case 'field_authors':<br />
      $title_classes[] = 'inline';<br />
      $content_classes[] = 'authors';<br />
      $item_classes[] = 'author';<br />
      break;<br />
  }</p>
<p>  // Apply odd or even classes along with our custom classes to each item */<br />
  foreach ($vars['items'] as $delta => $item) {<br />
    $vars['item_attributes_array'][$delta]['class'] = $item_classes;<br />
    $vars['item_attributes_array'][$delta]['class'][] = $delta % 2 ? 'even' : 'odd';<br />
  }<br />
}<br />

For consistency, we've structured this preprocess function just like preprocess_block from before. Let's break it down and see what we're doing.
Just like before, we create some shortcut variables to save us typing later. The first 3 variables, $name, $bundle and $mode, are what I like to call, context variables. They are used to target individual fields by name, bundle (think content type) and/or view mode (teaser, full etc.) The next four variables are class variables. They represent an array of classes for each of our four HTML elements: the outer field wrapper, label, content wrapper and an individual field items. I know what you're thinking, "Four divs for a single field??!!" Yeah, that's a lot of divs. I'll cover how to fix that in my next post. One thing at a time.
Next, we add global classes to all fields, including the ones that were originally hard coded in the core theme function before we removed them. Whether or not you want to add these classes is up to you. If you don't use them and want to slim down your HTML footprint, by all means, strip them out.
After our global classes, I like to keep commented print statements for each of our context variables handy. These simply print the value of the context variable to the screen. By uncommenting one of these lines, I can quickly see what values are attached to an individual field so I can target it properly.
Now we get into the meat of the function where we add classes to targeted fields. Keep in mind, this preprocess function runs on every field being rendered, so we need a way to distinguish which fields we are adding classes to. This is what the context variables are for. I like to use switch statements, nested where appropriate. In the example above, the first switch statement is checking to see if this field is being displayed in teaser mode. If so, do another switch on the field name. If the field name is node_link, add the more-link class. If it's the body or description field, two fields that often share semantic meaning across content types, add the description class.
The next switch statement is simply checking on the field name and adding classes regardless of view mode or content type. In this case, we are adding classes to the multi-valued authors field. We want the label to display inline, the content container to have a authors class and each item to have the author class.
Keep in mind that 'class' is just one attribute in the attributes array. You can extend this method further and start working with other HTML attributes such as rel or data. I've been super happy with this technique since I started using it. It's flexible, consistent and allows for granular control over field classes from the theme. The fact that it's in code, also makes it easier to migrate to and from different server environments.
Give this a try on your next project and let me know what you think.

Code Drupal Drupal Planet

Read This Next