Hi, first let me introduce myself and say hello, as this is my first time using Drupal. I am a freelance web designer/developer who has been using Xaraya almost exclusively for my CMS needs. I have tried Drupal on a couple of occasions and found it didn't quite suit my purposes, but with the release of Drupal 4.7 I feel it is time to get to know this CMS and so far I've found it to be excellent. In a short space of time I have created a working site with features I would normally have had to implement in a more laborious fashion. You've already saved me time because of the ease of implementation, so I thank you for that.

Now, on to my problem: I am unsure as to how to override a module's CSS. Currently I am wanting to style the Event module's output and am meeting failure in simply redeclaring the relevant css in my theme's stylesheet.

I have found a function in a forum post that allows me to disable the drupal.css file and I have then re-included a customised version of it with an import statement in my theme's css file. I would like to do the same for other css files imported by various modules I have installed. Does anyone know the best way to go about doing this?

Any help would be much appreciated and please let me know if I've posted in the wrong forum or otherwise made any mistakes.

Comments

tanc’s picture

I'd like to rephrase my question after doing some more investigation:

Does Drupal have a standard override system for CSS files that come with a module? Or do I have to work out how to override them in my template.php file? My objective is not to touch any code in the module's directory so that when it comes to upgrading the module every adjustment has been made in the theme's directory.

Does this make sense? In Xaraya there is a simple override system so if you place a css file in the correct directory it will automatically override the css file that comes with a module. From what I have read and examined it seems Drupal doesn't have this feature. It also looks like the API call to load the module's css is implemented differently by different module developers. This makes it tricky for me to work out how to override (or completely cancel) the loading of a module stylesheet.

I read a forum post where people were discussing a module's css file and they were simply changing the css file directly in the module directory. This seems like a bad idea to me but please correct me if I'm wrong.

kmo’s picture

You can quite easily "override" a module's CSS file by writing additional rules in the theme's style sheet.

This works because the CSS specification says that a rule that is specified in a "later" style sheet overrides a rule that is specified in an "earlier" style. In Drupal, the theme's style sheet is always included last.

For example, if a module's style sheet defines:

.event-calendar table {
  border: 1px solid #242;
  border-collapse: collapse;
  border-spacing: 0;
  width: 100%;
  margin-bottom: 1em;
}

You can simply override this by placing the following in the theme's style sheet:

.event-calendar table {
  /* whatever properties you want to override */
}

The only caveat is that you will need to make sure that the selector (in the example, .event-calendar table) is exactly the same as the one specified in the module's CSS. Otherwise strange things may result.

tanc’s picture

kmo, thanks for your reply. Overrides in this fashion are fine for minor changes but in my opinion it isn't suitable for heavy traffic sites where a number of modules and their associated style sheets are used as it doubles the amount of information the browser needs to load per css declaration (one module stylesheet then one overriding stylesheet). This increases the overhead and in some environments this can make a critical and even financial difference to the running of the web site.

What I'm looking for is a way to override the module's stylesheet before it is loaded by the browser so that a custom stylesheet is loaded in it's place. I'm curious to know how people achieve this as I'm sure this must be an issue for some people. Of course many people probably directly edit the module's stylesheet but again I feel this isn't appropriate as further module updates will need careful checking to ensure no new rules have been introduced or the stylesheet isn't accidentally over-written.

I'm beginning to think there is no clean solution to this problem from other forum posts. I'd like to see a better override system where a css file placed in the correct template directory would automatically override one in the associated module directory. Otherwise a solution in the phptemplate theme file would be adequate.

kmo’s picture

There is one solution using the page.tpl.php file in the template, but it's slightly clumsy.

In page.tpl.php, you should find a line similar to the following:

print $styles;

The $style variable contains all the style sheet import statements specified by Drupal and its modules, as well as the default style sheet (style.css) for the theme.

However, these style sheets also specified in an array returned by the theme_add_style function.

Combined with the theme_stylesheet_import hook, you could replace the code above with the following:

$stylesheets = theme_add_style();
foreach ($stylesheets as $stylesheet) {
  if ($stylesheet->path == "the path of the stylesheet you don't want") continue;
  print theme('stylesheet_import', $stylesheet->path, $stylesheet->media);
}

print theme('stylesheet_import', "the path of your overriding stylesheet");
bedlam’s picture

If, like me, you're a control freak when it comes to stylesheets, you won't want to use any default stylesheets unmodified. You also probably don't want to make modifications to the stylesheets provided with various modules directly, since you will either lose them or have to manually import them if you upgrade the module at any point. If, like me, you're also lazy, you won't want to keep opening your template.php file to add conditions checking for new stylesheets every time you add a new module.

Put all these requirements together, think about them for a few minutes, and you'll get a solution like this:

<?php
function themeName_stylesheet_import($stylesheet, $media = 'all') {

/*

  Requirements:
  -- no stylesheets imported except from the theme directory!
  -- no configuration should be required here when a new module is added!

*/
  
  $theme_directory_name = 'themeName';

  // 1. Check the incoming $stylesheet value for 'themeName':
  if (strpos($stylesheet, $theme_directory_name) !== false) {
    // 2. If we find it, then return 'theme_stylesheet_import($stylesheet, $media);' because we're done:
    return theme_stylesheet_import($stylesheet, $media);
  } else {
    $final_slash = strrpos($stylesheet, '/');

    // 3. Check for at least one '/' in the path:
    if ($final_slash !== false) {
      // 4. If '/' is present, truncate everything up to, and including the final one:
      $temp_stylesheet = substr($stylesheet, $final_slash+1);

      // 5. Prepend the resulting value with base_path() . 'sites/default/themes/themeName/':
      $output = base_path() . 'sites/default/themes/' . $theme_directory_name . '/' . $temp_stylesheet;

      // 6. Return the value.     
      return theme_stylesheet_import($output, $media);
    } 
  }
}
?>

Just rememeber to set $theme_directory_name to the actual directory name(s) you use in your theme.

If you're using a solution like this, and you find you actually do want to use a module's stylesheet unmodified, you can (if you have ssh access to your *nix server) simply create a symlink inside your theme directory to the stylesheet you want to use. For example, if your theme is located at /sites/default/themeName, and the module stylesheet is located at /sites/default/modules/someModule/module.css, navigate to /sites/default/themeName and type the following:

ln -s ../modules/someModule/module.css module.css

-b

-- b

ak’s picture

Just a small addition. Change $theme_directory_name = 'themeName'; to $theme_directory_name = path_to_theme(); and your theme directory and name will be located automatically. This will be more theme independent.

ak’s picture

Ups! Forgot one! This line $output = base_path() . 'sites/default/themes/' . $theme_directory_name . '/' . $temp_stylesheet; gets simpler and has to be changed as well, to $output = base_path() . $theme_directory_name . '/' . $temp_stylesheet;. No need to hardcode the theme location any more.

ak’s picture

This can get even better. Not quite sure how to implement it though. May you can do it. Check for file_exists( myModuleCSSinMyThemeDirectory ) if true load it, if not load the default css file of the module it self. No need for symlinks. Just lay back and throw a custom css file in your theme directory if needed. If there exists one it will be loaded if not we use the modules default. This could be one for the handbooks.

bedlam’s picture

Thanks for the API pointers and the idea--I didn't know about path_to_theme(). Here's a better version:

<?php

function myThemeName_stylesheet_import($stylesheet, $media = 'all') {
  /*------------------------------------------------------------
    Set up the variables we'll use in the function:
  ------------------------------------------------------------*/
  
  // Assign incoming value to a temp var so we can manipulate it without losing the original data: 
  $temp_stylesheet = $stylesheet;
  
  // Get the directory containing the current theme:
  $theme_directory_path = path_to_theme();
  
  // Get the full path to the theme directory from the webroot:
  $webroot_path_theme_directory = base_path() . $theme_directory_path . '/';
  
  // Get the full absolute path to the theme directory from root:
  $root_path_theme_directory = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $theme_directory_path . '/';
  
  
  /*------------------------------------------------------------
    Do the actual work:
  ------------------------------------------------------------*/
  
  // Test to see if the incoming stylesheet value contains the name of the current theme:
  if (strpos($stylesheet, $theme_directory_path) !== false) {
    // ...if it does, pass it through theme_stylesheet_import() without modification:
    return theme_stylesheet_import($stylesheet, $media);
  } else {
    // ...otherwise:
    
    // Test for the last slash in the $stylesheet:
    $final_slash = strrpos($stylesheet, '/');
    if ($final_slash !== false) {
      // ...if we find one, get the filename AND the preceding slash:
      $temp_stylesheet = substr($stylesheet, ($final_slash + 1));
    } // ...if we found no slashes in the filename, carry on...
    
    // Test to see if a file exists that corresponds to the path we've created:
    if (file_exists($root_path_theme_directory . $temp_stylesheet)) {    
      // ...if it does, pass is through the theme_stylesheet_import() function:      
      return theme_stylesheet_import($webroot_path_theme_directory . $temp_stylesheet, $media);
    } else {
      // ...but if it doesn't exist, pass the original value through the function:      
      return theme_stylesheet_import($stylesheet, $media);
    }
  }
}

?>

I dunno about the handbooks, I'd say the lack of something like this is a serious omission in the Drupal core--no doubt it would be possible to achieve much more efficiently in the core too.

In any case, the function is pretty much totally generic now though (as always) you'll have to change the first word of the function name to match the name of your theme.

-b

-- b

tanc’s picture

Thank you so much, this works perfectly.

Regards,
Tanc

Rowanw’s picture

Does this script work in Drupal 5? Do I have to name the new stylesheets in a special way or place them in a certain sub folder? It's not working for me.

m1mic’s picture

This script uses some functions that are not part of Drupal 5. Drupal 5 added a new function, drupal_add_css(), that will allow you to add to the array of style sheets. Please refer to the handbook page 66122 for an example of how to add this to your page.tpl.php file. Using the drupal_add_css() function in a page.tpl.php file will allow you to call a style sheet and have it only apply to that particular page layout. If it is something that will be universal throughout your site, use the drupal_add_css() function in the template.php file

bedlam’s picture

<rant>
This is still something that the Drupal core should be doing. The situation as it is now--even in version 5.x--is a major pain for theme developers and site-maintainers and, frankly, enforces poor css-coding practices. For example, if it's necessary/desirable to override the event module's default css in a theme, the only option available by default is to override event.css in the theme's stylesheet (i.e. since the module stylesheets should under no circumstances be edited directly--if the modules are ever updated, any such changes would be lost).

This results in a great number of duplicate css declarations being present in any theme where the various module defaults are, for one reason or another, unsuitable.

Furthermore, the default output of the css block in the document head is quite clueless--a separate style element for every single stylesheet?! What's wrong doing it this way:

<style type="text/css" media="all">
  @import "/lorem.css";
  @import "/ipsum.css";
  @import "/dolor.css";
</style>

... instead?

It could be argued that the default way provides a widely-compatible way of specifying the stylesheet's media, but since the default values are all "all", I'd think it would be better if the default were formatted as above and that theme authors could re-output them differently in the (less common) case where different media values are required.

I know that these defaults can be overridden--that's what this discussion is about--but as defaults, they're quite mediocre.
</rant>

In any case, anyone that's just read through my rant should be rewarded, so here's a new function that can be used to force Drupal to use theme-specific stylesheets when they are available. It looks through all of the available stylesheets and checks to see if there is a similarly-named file available in the current theme directory. If there is, that file is used instead. So, for example, a stock Drupal site may have a call to the file "/sites/default/modules/event/event.css", but this script replaces that call with a call to "/sites/default/themes/{theme name}/styles/event/event.css" The function uses Drupal 5 API calls:

/**
 *	This function replaces default Drupal stylesheets with theme-specific stylesheets IF they exist:
 */
function use_local_css_files($css,$media='all') {
	// Determine the path to the theme's styles directory from the web root:
	$webroot_theme_styles_directory = path_to_theme() . '/styles/';
	// Determine the path to the theme's styles directory from the absolute root:
	$root_theme_styles_directory = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $webroot_theme_styles_directory;
	// Make sure we start from a blank slate:
	$new_css_array = array();
	$new_default_css_array = array();
	$new_theme_css_array = array();
	// Loop through the incoming array of module css files--if there are any!:
	if (count($css[$media]['module']) > 0) {
		foreach($css[$media]['module'] as $key => $value) {
			// First, we have to strip out 'modules/' or 'sites/default/modules/':
			/*
				This regex says: replace any sub-strings containing:
					*- zero or more occurrences of "sites/default/" and
					*- one occurence of "modules/"
				...or in other words, any sub-strings containing:
					"sites/default/modules/" or "modules/"
				...with an empty string ('').
			*/
			$stylesheet_location = preg_replace('/(sites\/default\/)*(modules\/)/', '', $key);	
			// Put together the path from root (/) to the current stylesheet:
			$root_theme_stylesheet_path = $root_theme_styles_directory . $stylesheet_location;
			// Test to see if the local version exists:
			if (file_exists($root_theme_stylesheet_path)) { // When it does:
				// Build the path relative to the current theme:
				$theme_stylesheet_path = $webroot_theme_styles_directory . $stylesheet_location;
				// Add it to the array of THEME stylesheets:
				$new_theme_css_array[$theme_stylesheet_path] = $value;
			} else { // Otherwise:
				// Add the default stylesheet to the array of module stylesheets:
				$new_default_css_array[$key] = $value;
			}
		}
	}
	// Put everything together in the array format Drupal likes:
	$new_css_array[$media]['module'] = $new_default_css_array;
	$new_css_array[$media]['theme'] = array_merge($new_theme_css_array, $css[$media]['theme']);
	// Process everything we've got so far with drupal's main stylesheet function:
	$output = drupal_get_css($new_css_array);
	// Return the css calls:
	return $output;
}

Add this function to your template.php file, and call it in page.tpl.php. The function processes one media type at a time, so you may need to run it more than once (this is probably not ideal, but I only discovered how media types were stored in the css array after I wrote the basic function...this was the easiest way to support other media types...) Sample code follows:

	// Add a print stylesheet to the theme:
	$css = drupal_add_css(path_to_theme() . '/styles/print.css', 'theme', 'print', true);
	// Output all the 'media="all"' stylesheets:
	echo use_local_css_files($css);
	// Output all the 'media="print"' stylesheets:
	echo use_local_css_files($css, 'print');

Tested and apparently working with Drupal 5.1.

-- b

OneSeventeen’s picture

Warning: my method is not good!

I changed the webroot_themes_style_directory to not include the trailing "styles/" (note I left the trailing "/" before "/styles/" in the code, otherwise it won't work later on down the road.)

To make it work, I also changed the $stylesheet_location to be just the stylesheet filename.

Here's how I get just the stylesheet filename:

<?php
  $stylesheet_exploded = explode("/", $key);
  $stylesheet_location = array_pop($stylesheet_exploded);
?>

The reason I do this, is because I'm keeping my site fairly simple and just want to be able to see a filename, like "user.css", and copy it to my theme folder, call it "user.css" and have it override the default one.

This way I don't have to manage /styles/user/user.css or anything like that.

This is bad for many many reasons, not the least of which is the performance hit of turning each file path into an array to pop off the end. (There's also the danger of multiple .css files with the same filename but different paths.)

I just thought I'd point out how to be lazy with poor code :)

finius_lyn’s picture

Sorry for the very simple question. hopefully this will get me a very simple answer.
;-)
This looks like a fantastic fix for something that I am trying to do - replace module style sheets with custom versions, *without* having to dig into them directly.
My only question is this: How do I implement this fix? I assume that I add the large php function into my theme's template.php file. Where do I call the function and what variables do I need to pass? Does it replace the 'print $styles;' call in the page.tpl.php file?
Again, I apologize and offer many thanks in advance.

PS: I am running drupal 6.9
Take care,
Finius

dvessel’s picture

I just put up this handbook on this and I think it's the most efficient. 2 approaches.. the first is best I think. It loads up modified module styles on top of the original when it's detected for a given page. Drupal 5 preprocesses the styles and loading the originals makes almost no difference. Browsers will only consider the last ruleset so it won't affect performance. There will be a bit more to load but it's not a big factor in *most* cases. It's better than stuffing a bunch of styles in a single location (style.css). Easy to track just by copying for example node.css from the modules folder to the styles directory in your theme named "mod-node.css" and placing just the stuff you want that's node related.

The other is to do exactly what you describe Tanc. Replace originals. I've used it a bit but replacing everything is usually a waste of time. Completely replacing the styles just means more to manage in the end since most themers will only override a fraction of the style rules but of course your milage may vary.

http://drupal.org/node/155400

alandove’s picture

Hey, folks:

Maybe I'm taking an extreme position here, but I wanted to build a new theme more or less from scratch, and I was appalled at how hard Drupal makes this. Other CMSs (e.g. Joomla) are smart enough to check the theme/template directory for custom templates automatically, making overrides trivial - just put a file in the theme folder with the same name, and it gets used instead of the default. Drupal has a jaw-dropping quantity of presentational cruft coming from the core, and the workarounds all feel like ugly hacks. So I came up with one even uglier. This approach certainly isn't for everyone, but I found it pretty eye-opening. I ran the following Bash script inside Drupal's directory on a test site I didn't mind breaking - running it anywhere else would be disastrous:

#!/bin/bash
# This script alters your Drupal installation irreversibly.
# If you don't understand EXACTLY what the code below does, DON'T USE IT!

NEWCSS="*{margin: 0; padding: 0; }"

# Get the current themes out of harms' way and get ready to save the ones we're going to zap.
touch ../rescuedstyles.css
mv themes ..

# Find all the stylesheets that are mixed in with the core.
find . -name "*.css" > csspaths.txt
FILECOUNT=$(wc -l csspaths.txt | awk '{print $1}')

# Iterate through those stylesheets and wipe 'em out.
for (( i=1; i<=FILECOUNT; i++ ))
do
FILENAME=$(sed -n ${i}p csspaths.txt)
cat $FILENAME >> ../rescuedstyles.css
echo $NEWCSS > $FILENAME
done

# Put things back where they belong and clean up
# The rescuedstyles.css file will contain all of the removed styles
mv ../themes themes
mv ../rescuedstyles.css themes/
rm csspaths.txt

Now I have a file called rescuedstyles.css with all of the CSS the core used to excrete - all 1,075 lines of it (!!), and the core modules put out nothing but global CSS resets. Of course, the site looks quite bare, and this probably isn't a practical solution for most folks, but now it's very easy for me to include or override whatever I want. When upgrading, I'll just run the same script on the new version.

GSnyder’s picture

Note that Drupal 6 has its own way of doing this kind of override. See http://drupal.org/node/263967.