DiggingIntoWordPress

by Chris Coyier & Jeff Starr

Specify Unique CSS File Per Post

Posted by on

I’m a HUGE fan of being able to link up a CSS file on a per-page basis. I just find it extremely common that a page needs CSS styling unique to it, and I hate litering a sites main stylesheet with customizations that only one particular page needs. We’ve talked about this before, and even created a custom method for doing so, as well as mentioned the art direction plugin, which makes this easily possible.

Both of those methods are still sweet, but I find in general that I like to write my CSS in actual CSS files I keep in my theme and can edit using whatever CSS editor I like, as well as keep things clean and simple in the admin. So, I’ve altered the technique a bit.

The idea is to add a simple text input to the page editor, right underneath the content area, where you can manually specify the name of a CSS file. In the image below, you can see I’ve also added one for JavaScript.

To add this to your site, it’s just some code to add to the functions.php file. Should work for any site.

// Custom Stylesheet box in Pages/Posts
add_action('admin_menu', 'digwp_custom_css_hooks');
add_action('save_post', 'digwp_save_custom_css');
add_action('wp_head','digwp_insert_custom_css');
function digwp_custom_css_hooks() {
	add_meta_box('custom_css', 'Name of custom CSS file', 'digwp_custom_css_input', 'post', 'normal', 'high');
	add_meta_box('custom_css', 'Name of custom CSS file', 'digwp_custom_css_input', 'page', 'normal', 'high');
}
function digwp_custom_css_input() {
	global $post;
	echo '<input type="hidden" name="custom_css_noncename" id="custom_css_noncename" value="'.wp_create_nonce('custom-css').'" />';
	echo '<input type="text" name="custom_css" id="custom_css" style="width:100%;" value="'.get_post_meta($post->ID,'_custom_css',true).'" />';
}
function digwp_save_custom_css($post_id) {
	if (!wp_verify_nonce($_POST['custom_css_noncename'], 'custom-css')) return $post_id;
	if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return $post_id;
	$custom_css = $_POST['custom_css'];
	update_post_meta($post_id, '_custom_css', $custom_css);
}
function digwp_insert_custom_css() {
	if (is_page() || is_single()) {
		if (have_posts()) : while (have_posts()) : the_post();
		  $filename = get_post_meta(get_the_ID(), '_custom_css', true);
		  if ($filename) {
			echo "<link rel='stylesheet' type='text/css' href='" . get_bloginfo('template_url') . "/css/" . $filename . "' />";
          }
		endwhile; endif;
		rewind_posts();
	}
}

If you are happy with just CSS only, you can stop there. If you would like the JavaScript box as well, here is that code.

// Custom JavaScript in Pages/Posts  (...lots of repeated code, could be better)
add_action('admin_menu', 'digwp_custom_js_hooks');
add_action('save_post', 'digwp_save_custom_js');
add_action('wp_head','digwp_insert_custom_js');
function digwp_custom_js_hooks() {
	add_meta_box('custom_js', 'Name of custom JavaScript file', 'digwp_custom_js_input', 'post', 'normal', 'high');
	add_meta_box('custom_js', 'Name of custom JavaScript file', 'digwp_custom_js_input', 'page', 'normal', 'high');
}
function digwp_custom_js_input() {
	global $post;
	echo '<input type="hidden" name="custom_js_noncename" id="custom_js_noncename" value="'.wp_create_nonce('custom-js').'" />';
	echo '<input type="text" name="custom_js" id="custom_js" style="width:100%;" value="'.get_post_meta($post->ID,'_custom_js',true).'" />';
}
function digwp_save_custom_js($post_id) {
	if (!wp_verify_nonce($_POST['custom_js_noncename'], 'custom-js')) return $post_id;
	if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return $post_id;
	$custom_js = $_POST['custom_js'];
	update_post_meta($post_id, '_custom_js', $custom_js);
}
function digwp_insert_custom_js() {
	if (is_page() || is_single()) {
		if (have_posts()) : while (have_posts()) : the_post();
		    $filename = get_post_meta(get_the_ID(), '_custom_js', true);
		    if ($filename) {
			 echo "<script type='text/javascript' src='" . get_bloginfo('template_url') . "/js/" . $filename . "' ></script>";
            }
		endwhile; endif;
		rewind_posts();
	}
}

You could probably combine these together a bit better than I have done here, it’s a bit redundant.

How it Works

There are three things going on here, which tap into three existing WordPress “hooks”.

  1. Adds text inputs to the page editing screen of the admin using the admin_menu hook and the add_meta_box function
  2. Save the values of those text inputs when the page is saved using the save_post hook
  3. Output <link> or <script> tags in the <head> of the site (when those values are present) using the wp_head hook.

The Result

If you put the value “yeahbaby.css” in that input box, when that particular page loads, you are going to get this output in the <head>:

<link rel='stylesheet' type='text/css' href='http://yoursite.com/notes/wp-content/themes/your-theme/css/yeahbaby.css' />

“yoursite.com” will obviously be your sites domain, and your-theme will the folder in which your currently active theme resides. Do keep that in mind, as should you change themes, you’ll need to move the “css” folder in that theme that contains these custom CSS files to the new theme.

UPDATE (May 13, 2010)

Notice how all the function names are prefixed by “digwp_” – that is in accordance to best practices described by Andrew Nacin here.

This has also been made into a plugin by Utkarsh Kukreti (404 link removed 2014/05/30). Requires PHP5.

Ways This Code Could Be Improved

I was holding back on publishing this because there are a lot of ways it could be better. But I had mentioned it at the CMS Expo (404 link removed 2014/02/14) and there was some interested and I said I would, so I’m releasing it a bit early. Here are things that could be better…

  • Combine the functions together to create less redundancies between the CSS and JavaScript versions.
  • Allow for a comma-separated list in case multiple custom CSS files are needed.
  • Make it a plugin instead of custom functions. Add options screen to specify file path to custom CSS folder.

21 Responses

  1. Hesham May 10, 2010

    Thank you so much for this code, I was trying to find something similar, the great thing here is you can expand the code to make more of it :)

    I will give it a try soon on one of my projects

  2. I’m with Hesham. I’m learning quickly (thanks for that as well), but this one was bugging me. I’ve used the Art Direction plugin, but I hate to copy/paste code, and wanted to just find a way around it.

    Thanks to you both for keeping this thing going. It’s a ton of help.

  3. Great explanation. I’ve googled my eyes out for a decent write-up of how to add custom meta boxes to the admin panels, and how to use the data in the templates. Though I hate writing HTML in PHP’s echo() method, an include will do it for longer pieces.

  4. Ian Storm Taylor May 10, 2010

    Chris Coyier… how are you always the shit?! Thank you sir.

  5. Jaspio May 11, 2010

    Great idea. I’d like to adapt this code to make the CSS change based on categories or tags.

    I’ve got a new project! ;P

  6. Actually, no offense but I find it better to use an extended body class function for example this is what I do with ComicPress:

    add_filter('body_class','comicpress_body_class');
    
    function comicpress_body_class($classes = '') {
    	global  $current_user, $is_lynx, $is_gecko, $is_IE, $is_opera, $is_NS4, $is_safari, $is_chrome, $is_iphone, $post, $wp_query, $comicpress_options;
    
    	if (!empty($current_user)) {
    		$user_login = addslashes($current_user->user_login);
    		if (!empty($user_login)) $classes[] = 'user-'.$user_login;
    	} else {
    		$classes[] = 'user-guest';
    	}
    
    	if (function_exists('comicpress_is_member')) {
    		if (comicpress_is_member()) {
    			$classes[] = 'sitemember';
    		} else {
    			$classes[] = 'non-sitemember';
    		}
    	}
    		
    	if (is_single() && !is_attachment()) {
    		if (in_comic_category()) {
    			$classes[] = 'comic';
    		} else {
    			$classes[] = 'noncomic';
    		}
    	}
    
    	if($is_lynx) $classes[] = 'lynx';
    	elseif($is_gecko) $classes[] = 'gecko';
    	elseif($is_opera) $classes[] = 'opera';
    	elseif($is_NS4) $classes[] = 'ns4';
    	elseif($is_safari) $classes[] = 'safari';
    	elseif($is_chrome) $classes[] = 'chrome';
    	elseif($is_IE) $classes[] = 'ie';
    	else $classes[] = 'unknown';
    	if($is_iphone) $classes[] = 'iphone';
    
    
    // Hijacked from the hybrid theme, http://themehybrid.com/
    	if (is_single()) {
    		foreach ( (array)get_the_category( $wp_query->post->ID ) as $cat ) :
    			$classes[] = 'single-category-' . sanitize_html_class( $cat->slug, $cat->term_id );
    		endforeach;
    		$classes[] = 'single-author-' . get_the_author_meta( 'user_nicename', $wp_query->post->post_author );
    	}
    
    	if ( is_sticky( $wp_query->post->ID ) ) {
    		$classes[] = 'sticky-post';
    	}
    
    // NOT hijacked from anything, doi! people should do this.
    	$rightnow = date('Gi');
    	$ampm = date('a');
    	$classes[] = $ampm;
    
    	if ((int)$rightnow > 559 && (int)$rightnow < 1800) $classes[] = 'day';
    	if ((int)$rightnow < 600 || (int)$rightnow > 1759) $classes[] = 'night';
    	
    	if ((int)$rightnow > 2329 || (int)$rightnow < 0030) $classes[] = 'midnight';
    	if ((int)$rightnow > 0559 && (int)$rightnow < 1130) $classes[] = 'morning';
    	if ((int)$rightnow > 1129 && (int)$rightnow < 1230) $classes[] = 'noon';
    	if ((int)$rightnow > 1759 && (int)$rightnow < 2330) $classes[] = 'evening';
    	
    	$classes[] = strtolower(date('D'));
    
    	if ( is_attachment() ) {
    		$classes[] = 'attachment attachment-' . $wp_query->post->ID;
    		$mime_type = explode( '/', get_post_mime_type() );
    		foreach ( $mime_type as $type ) :
    			$classes[] = 'attachment-' . $type;
    		endforeach;
    	}
    
    	$classes[] = 'layout-'.$comicpress_options['cp_theme_layout'];
    
    	return $classes;
    }

    This way when doing styling you can not only change your CSS for each and every page you have every variation in between, for example

    body.home { background: #000; }

    Will change the background for the home and:

    body.page-id-3434 { background: #ccc; }

    Will change something specifically for that page.

    .category-random-thoughts .post  { 
           background: #999;
    }

    You can specify categories like that.

    Even more you can do with extended body classes as noted in the code above you can set the time of the day even week to change the site design.

    – Phil

    • Adding special body classes is way awesome, I love that approach too. But I think it works best in conjunction with unique stylesheets. With body classes alone you would need to put everything in one main stylesheet which could bloat really quickly if all special one-off pages CSS needed to be in there.

      • While I agree on bloating there is no more bloating then adding additional css stylesheet, not to mention that gzip and other compression for each .css load is not recommended.

        Another reason to favor body classes is the congruent links to additional elements that could benefit from the same style, in essence optimizing it.

        In your case your actually adding to the overhead by not cross sharing the elements between the different pages that could use it.

        Here is the yslow output for your site this page in particular:
        http://frumph.net/stuff/digwp.htm

        What this shows an overall excellent score, but imagine adding more css to that for your styling and additional elements. The page above doesn’t reflect that but I’m fairly certain if you go to http://jigsaw.w3.org/css-validator/ and check this page out you will see a large list of incongruities. Usually its the same element that doesn’t need a change in the background color, which I’m famously as well for doing.

        I read your other post thanks to the comment below about your body_class_plus and noticed that it was not adding to the current body_class and should be used via filter, might be something to look into changing over to.

        There are also post_class and comment_class filters available to further even those, including microids and odd/even classes, which you cannot do with just adding an individual stylesheet for.

        The point is, while I agree this is a great way to separate and control the environment it might not be for everyone, and the body_class filter probably should be mentioned as well.

        – Phil

  7. Nathan B. May 11, 2010

    Great, thanks for this.!!

    Sorry if I missed this, but does this add a custom style sheet in addition to the theme’s style.css, so that yeahbaby.css is called after style.css (thus overriding it, right?) in the header — or does it replace it, so that yeahbaby.css must include the styles of the default stylesheet, plus additions and changes? I’d prefer both to be called and yeahbaby.css to overwrite the default style.css.

    BTW, are you essentially endorsing Kerrick’s approach over Hassan’s from Feb 8? (bit.ly/aOXkhX) I read you going the other way on that in that post.

    • It adds the new stylesheet whereever the wp_head() function is, which in my themes is typically after the main stylesheet call.

      I endorse any technique that gets the job done well for the specific scenario =)

  8. No no no! Don’t echo out the script/css tags as text. Enqueue them so that other plugins (such as caching or minifying plugins) can process them. The functions you need are wp_enqueue_script and wp_enqueue_style.

  9. Scott Corgan May 11, 2010

    Looks useful, BUT, good designs and code can be globally used, realy.

  10. WordPresser May 18, 2010

    A global CSS gets loaded at first and then any class call from any page would take it from the cached CSS file. This makes navigation in the site more responsive than loading a CSS file with each page.

  11. Kerry Coppa May 28, 2010

    web browsers which could do no more. Well, now we have browsers that can do much more. Yep, there are problems, but there is a Chinese proverb which says something to the effect of, “Progress consists of trading new problems for old ones.”

  12. Was looking for something like this. Thanks

  13. I love this.

    In practice, when I tried it on a post, I realized that when that post is showing up in other pages–such as the main blog loop, or I suppose archive pages, etc., it will not have those styles.

    (I’m sure in some cases, this is exactly what you would want.)

    Anyway, it would be cool to create a way where it would apply the stylesheet whenever that post showed up, even in one of the other pages.

    Hmm, I suppose that takes a lot of logic because you need to load the stylesheets in the head, so you’d need to kind of go through the loop when writing out the head, and that would be odd. I guess in this case, it would make more sense to use a different approach.

  14. Thanks for sharing this! A valuable reference. I also appreciate the tip and link on creating a custom prefix. I am checking out WP 3.0 RC1 and trying to be more WP savvy.

Comments are closed. Contact us with any critical information. Thank you!

Code is poetry