Supercharging CSS, Part 3: CSS Variables

Previous: Themes

It is often the case when skinning or theming a new version of a site that most of the CSS is the same and all we'll do is change a few colors and little else. Each site will tend to have a palette of ten or less colours (usually five or less) that are subject to this kind of change. Yet there will be numerous references to the same values and it's tedious and error-prone to change.

This has long been an issue for Web designers and developers so much so that there is now a proposed standard for their implementation. The problem with any new CSS feature is of course browser support. CSS 2 was introduced in 1998 (although CSS 2.1 didn't become a candidate recommendation until almost nine years later) and we still can't rely on total support for it.

The proposed standard would use syntax such as this:

@variables {
  CorporateLogoBGColor: #fe8d12;
}

div.logoContainer {
  background-color: var(CorporateLogoBGColor);
}

Although this issue is a decade old, agreement is far from universal. Some argue CSS variables are harmful while others argue CSS variables are unnecessary.

My personal experience has been that stylesheets often become large and unwieldy. Retheming a site becomes a daunting prospect. I see CSS variables as nothing more than the declaration of semantic intent or context no different to using constants in programming languages. In other words, they have the potential to make stylesheets far more readable and maintainable. We can't wait for IE9 and FF4 to be the baseline for browsers however so we have to do this ourselves. PHP to the rescue.

The solution I'm proposing here isn't new. Dynamically generated stylesheets with variable substitution are not a new idea. I'm simply extending the script developed thus far to include this capability and explaining what it is we're trying to achieve and why.

Rather than copy the proposed syntax for CSS Variables, which would be non-trivial to parse and process reliably, I am going to use a far more "PHP-ish" solution and use variables beginning with a dollar sign, as this is a rarely occurring character in stylesheets.

<?php
define('SCRIPT_DIR', $_SERVER['DOCUMENT_ROOT'] . '/css/');
define('CACHE_DIR', $_SERVER['DOCUMENT_ROOT'] . '/cache/');
define('PROPERTIES_EXTENSION', '.properties');

$bundles = array(
  'site' => array(
    'superfish.css',
    'tooltips.css',
    'site.css',
  ),
);

$site = $_GET['site'];
if (!isset($bundles[$site])) {
  error_log("css.php: Unknown bundle '$site' requested");
  exit;
}

$mtime = $_GET['mtime'];
$cache_file = CACHE_DIR . $site . '.css';
$cache_mtime = @filemtime($cache_file);

// we need to rebuild is the passed in mtime is newer than the cache file mtime
if ($mtime > $cache_mtime) {
  $css = '';
  foreach ($bundles[$site] as $file) {
    $contents = @file_get_contents(SCRIPT_DIR . $file);
    if ($str === false) {
      error_log("css.php: Error reading file '$file'");
    } else {
      $css .= $contents;
    }
  }
  $css = replace_variables($site, $css);
  file_put_contents($cache_file, $css);
} else {
  $css = file_get_contents($cache_file);
}

header('Content-Type: text/css');
header('Expires: ' . gmdate('D, d M Y H:i:s', time()+365*24*3600) . ' GMT');
header('ETag: "' . md5($scripts) . '"');

if (is_buggy_IE()) {
  ob_start();
} else {
  ob_start('ob_gzhandler');
}

echo $css;

function is_buggy_IE() {
  $ret = false;
  $agent = $_SERVER['HTTP_USER_AGENT'];
  if (strpos($agent, 'Mozilla/4.0 (compatible; MSIE ') === 0 && strpos($ua, 'Opera') === false) {
    $version = floatval(substr($agent, 30));
    if ($version < 6) {
      $ret = true;
    } else if ($version == 6 && strpos($agent, 'SV1') === false) {
      $ret = true;
    }
  }
  return $ret;
}

function replace_variables($site, $css) {
    global $css_variables; // this needs to be accessible
    $file = SCRIPT_DIR . $site . PROPERTIES_EXTENSION;
    $contents = file_get_contents($file);
    $lines = explode("\n", $contents);
    $css_variables = array();
    foreach ($lines as $line) {
        $line = preg_replace('!//(.*)$!', '', $line); // allow for comments
        $line = trim($line);
        if (!$line) {
            continue;
        }
        list($k, $v) = explode('=', $line);
        $k = trim($k);
        $v = trim($v);
        if (isset($css_variables[$k])) {
            die("Variable '$k' already set");
        }
        if (!preg_match('!^[\w_]+!', $k)) {
            die("Illegal variable '$k'. Must be letters, digits or underscore only.");
        }
        $css_variables[$k] = $v;
    }
    return preg_replace_callback('!\$([\w_]+)!', 'replace_variable', $css);
}

function replace_variable($matches) {
    global $css_variables;
    $var = $matches[1];
    if (!isset($css_variables[$var])) {
        // if we're strict, we could die here
        //die("Unknown variable '$var' encountered in CSS");
        // more loosely we could just reeturn the expression unchanged
        return '$' . $var;
    }
    return $css_variables[$var];
}
?>

This script will load some values from .properties and substitute $variables from the CSS for those values. Once again this behavior can be extended to get values based on user preferences, from the database or whatever you want to implement. The end result is browser-compatible yet much more powerful than "plain" CSS.

What about the performance? The use of regex replacement is quite cheap in this situation--much cheaper than minification is from the Javascript example. Because the end result is cached so aggressively any such cost is minimized by virtue of the client just not getting it that often.

Once more, feel free to use the code in any way you wish. Drop me a line or leave a comment if this was helpful to you or you find an issue or simply have a suggestion.

1 comments:

Priteh Taral said...

your Site is very useful , specially CSS section .
Keep posting Such brilliant and innovative ideas.

Post a Comment