How to add custom CSS and javascript to WordPress posts

If an individual post or page in your site requires particular presentational changes or whizz-bang effects, it can be handy to be able load up some custom CSS or javascript, either using internal <style> or <script> tags, or linking to external files.

While there are plugins available, a plugin-less solution is fairly easy to implement with just some wee modifications to the active theme. We’ll be using custom fields to store the scripts and styles, and setting up meta boxes on the post editing screen to make the editing process nice and simple.

1. Creating the custom fields

We’ll be using 4 custom fields, for external CSS, internal CSS, external javascript, and internal javascript. These will be called ‘css’, ‘css_internal’, ‘js’ and ‘js_internal’ respectively.

To kick off, start by creating or editing a post. In the ‘Custom fields’ section, create new custom fields using each of the above keys. For external files, we want to specify a comma-separated list of files, either as absolute urls or relative to the current theme directory. For internal CSS/js we’ll just be pasting the code straight in.

The values for the custom fields will look something like this:

css
stylesheet1,stylesheet2
js
script1, http://cdn.example.com/some-kewl-script
css_internal
#some-element { color: red }
js_internal
alert(‘hello!’)

Note I’ve left off the extensions for the external scripts and stylesheets – the code to implement this should be able to add the default .css or .js extensions if left out.

Adding a custom field for external javascript files

Adding a custom field for external javascript files

2. Getting the scripts and styles onto the page

Easy enough to add custom fields, no? Now let’s have a look at how to get our defined fields to show on the page. We can do this using WordPress’ add_action¬†function. I’m going to insert the CSS in the page’s <head> element, and the javascript at the bottom of the page (this is both for fast loading, and to ensure that any dependencies such as jQuery are loaded before our custom scripts).

Add this code to your theme’s functions.php file:

add_action('wp_head', 'external_post_css', 100);
add_action('wp_head', 'internal_post_css', 101);
add_action('wp_footer', 'external_post_js', 100);
add_action('wp_footer', 'internal_post_js', 101);

The first parameter specifies the name of the action to which the function is added, the second specifies the name of the function that we’ll be using, and the third is the order in which the function should be called. I’ve used a fairly high number so that the scripts and stylesheets will be added after any default CSS or javascript, and that any internal stuff will be loaded after external files.

However I prefer to modify this just slightly. Creating additional functions in a theme can run the risk of causing fatal errors if a plugin is added later on that contains functions with the same name. The solution is to either use an obscure prefix, or encapsulate custom functions within a static class to reduce the possibility of collisions. I personally prefer the latter approach, so the code would be modified thus:

add_action('wp_head', array('mytheme', 'external_post_css'), 100);
add_action('wp_head', array('mytheme', 'internal_post_css'), 101);
add_action('wp_footer', array('mytheme', 'external_post_js'), 100);
add_action('wp_footer', array('mytheme', 'internal_post_js'), 101);

If you’re not familiar with this syntax, passing an array to the function argument means that the action will trigger the method ‘external_post_css’ of the class ‘mytheme’ for the first line, and so on.

All good, now let’s build the functions. These will all be static functions of the class ‘mytheme’ which will also be added to functions.php:

class mytheme {

    // utility function for getting external css/javascript files
    // gets the string value of the custom field,
    // and converts the comma-separated string to an array of file uri's
    // the $type argument will be either 'css' or 'js'
    protected static function parse_post_files($type) {

        $ret = array();

        // only go ahead if we're on an individual post or page
        if (is_single()) {

            // use global post object as we're outside of the loop
            global $post;
            if ($data = get_post_meta($post->ID, $type, true)) {

                // split string by commas
                // trim each entry so it won't go pear-shaped if there's a space around a comma
                $files = explode(',', $data);
                $files = array_map('trim', $files);

                foreach ($files as $file) {
                    // if the file is given as an absolute uri, then use that uri as it is
                    // otherwise prepend theme directory uri
                    $loc = preg_match('/^https?:/', $file)
                        ? $file
                        : get_template_directory_uri() . '/' . $file;

                    // append default .css or .js extension if an extension has not been specified
                    $loc .= preg_match('/\.\w+(\?.*)?$/', $loc) ? '' : '.' . $type;
                    $ret[] = $loc;
                }
            }
        }
        return $ret;
    }

    // add a <link> tag for each external css file
    public static function external_post_css() {

        $files = self::parse_post_files('css');
        foreach ($files as $file) {
            ?><link rel=stylesheet href="<?php echo $file; ?>"><?php
        }
    }

    // add a <script> tag for each external js file
    public static function external_post_js() {

        $files = self::parse_post_files('js');
        foreach ($files as $file) {
            ?><script src="<?php echo $file; ?>"></script><?php
        }
    }

    // add any internal css in a <style> tag
    public static function internal_post_css() {

        if (is_single()) {
            global $post;
            if ($internal_css = get_post_meta($post->ID, 'internal_css', true)) {
                ?><style><?php echo $internal_css; ?></style><?php
            }
        }
    }

    // add internal javascript in a <script> tag
    public static function internal_post_js() {

        if (is_single()) {
            global $post;
            if ($internal_js = get_post_meta($post->ID, 'internal_js', true)) {
                ?><script><?php echo $internal_js; ?></script><?php
            }
        }
    }
}

Now any CSS or javascript specified in the custom fields for a post will be automagically inserted into the page!

3. Creating a meta box for the custom fields

This all works fine, but the custom field editor isn’t really all that pretty, and you don’t get any instructions on what each field is and what’s meant to go in it, which could be potentially confusing later on. Let’s finish off by tarting up the fields into custom meta boxes.

First we need a couple more add_actions, one to add a meta box to the page and post editing screens, and another to save the fields:

add_action('add_meta_boxes', array('mytheme', 'add_css_js_meta_box'));
add_action('save_post', array('mytheme', 'add_css_js_save_postdata'));

Next, add the following functions to the mytheme class:

class mytheme {

    // stuff we've already added

    // add meta box to page and post editing screens
    public static function add_css_js_meta_box() {

        add_meta_box(
            'css_js_meta_box',
            __('Custom CSS / javascript'),
            array('mytheme', 'css_js_meta_box'),
            'post'
        );
        add_meta_box(
            'css_js_meta_box',
            __('Custom CSS / javascript'),
            array('mytheme', 'css_js_meta_box'),
            'page'
        );
    }

    // create the form fields for the meta box
    public static function css_js_meta_box($post) {

        // Use nonce for verification
        wp_nonce_field( plugin_basename( __FILE__ ), 'custom_css_js_nonce' );

        ?><p>
            <label for="external_css">External CSS</label>
            <textarea name="external_css" id="external_css" rows="4" cols="50" style="width:98%"><?php
            echo esc_html(get_post_meta($post->ID, 'css', true));
            ?></textarea>
            <small>External css files, comma-separated. Can be specified relative to theme directory
                or as absolute urls. '.css' extension will be added if not specified.</small>
        </p>
        <p>
            <label for="external_js">External JS</label>
            <textarea name="external_js" id="external_js" rows="4" cols="50" style="width:98%"><?php
            echo esc_html(get_post_meta($post->ID, 'js', true));
            ?></textarea>
            <small>External javascript files, comma-separated. Can be specified relative to theme directory
                or as absolute urls. '.js' extension will be added if not specified.</small>
        </p>
        <p>
            <label for="internal_css">Internal CSS</label>
            <textarea name="internal_css" id="internal_css" rows="4" cols="50" style="width:98%"><?php
            echo esc_html(get_post_meta($post->ID, 'internal_css', true));
            ?></textarea>
            <small>Anything put here will be placed in a style tag in the page head.</small>
        </p>
        <p>
            <label for="internal_js">Internal JS</label>
            <textarea name="internal_js" id="internal_js" rows="4" cols="50" style="width:98%"><?php
            echo esc_html(get_post_meta($post->ID, 'internal_js', true));
            ?></textarea>
            <small>Anything put here will be placed in a script tag in the page footer.</small>
        </p>
        <?php
    }

    // save submitted data
    public static function add_css_js_save_postdata($post_id) {

        // http://codex.wordpress.org/Function_Reference/add_meta_box

        // verify if this is an auto save routine.
        // If it is our form has not been submitted, so we dont want to do anything
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
            return;

        // verify this came from the our screen and with proper authorization,
        // because save_post can be triggered at other times
        if (!wp_verify_nonce( $_POST['custom_css_js_nonce'], plugin_basename( __FILE__ )))
            return;

        // Check permissions
        if ('page' == $_POST['post_type']) {
            if (!current_user_can('edit_page', $post_id))
                return;
        }
        else {
            if (!current_user_can('edit_post', $post_id))
                return;
        }

        // OK, we're authenticated: we need to find and save the data

        update_post_meta($post_id, 'css', $_POST['external_css']);
        update_post_meta($post_id, 'js', $_POST['external_js']);
        update_post_meta($post_id, 'internal_css', $_POST['internal_css']);
        update_post_meta($post_id, 'internal_js', $_POST['internal_js']);
    }
}

While last step is not necessary, it surely looks a lot nicer and makes the fields easier to use.

Lazy alternative

If you can’t be arsed with step 3, you can achieve a similar result using the very wonderful Advanced Custom Fields plugin. With this plugin, we can specify a label and instructions for each field:

Adding a label and instructions for the CSS custom field

Adding a label and instructions for the CSS custom field

CSS custom field, via ACF

CSS custom field, via ACF