Adding stylesheets (CSS) and JavaScript (JS) to a Drupal 8 theme

Last updated on
10 February 2017

This documentation is incomplete. Add more information.

In Drupal 8, stylesheets (CSS) and JavaScript (JS) are loaded through the same system for modules (code) and themes, for everything: asset libraries.

Drupal uses a high-level principle: assets (CSS or JS) are still only loaded if you tell Drupal it should load them. Drupal does not load every assets on every page, because it slows down front-end performance.

Differences from Drupal 7

There are four important differences compared to Drupal 7 for themers:

  1. The THEME.info.yml  file has replaced the  THEME.info file (with the same data).
  2. The stylesheets property (for adding CSS) in THEME.info.yml has been removed and replaced by *.libraries.yml. 
  3. The scripts property (for adding JS) in THEME.info.yml has been removed and also replaced by *.libraries.yml.
  4. Only CSS, JS that is required on a page will be loaded. JQuery, for example is no longer automatically loaded unless explicitly specified in *.libraries.yml. If your theme requires jQuery or other assets you want to load on all pages, add them in *.libraries.yml.

The process

To load CSS or JS assets:

  1. Save the CSS or JS to a file using the proper naming conventions and file structure.
  2. Define a "library", which can contain both CSS and JS files.
  3. "Attach" the library to all pages, to specific Twig templates, or to a render element in a preprocess function.

Defining a library

Define all of your asset libraries in a *.libraries.yml file in your theme folder. If your theme is named fluffiness, the file name should be fluffiness.libraries.yml Each "library" in the file is an entry detailing CSS and JS files (assets), like this:

cuddly-slider:
  version: 1.x
  css:
    theme:
      css/cuddly-slider.css: {}
  js:
    js/cuddly-slider.js: {}

In this example the JavaScript: cuddly-slider.js is located in the js directory of your theme. JS can also come from an external URL or from included CSS files. See CDN / externally hosted libraries for details on attaching external libraries.

Remember, Drupal 8 no longer loads jQuery on all pages by default, so for example if cuddly-slider needs JQuery you must declare a dependency  on the core library that contains jQuery (Drupal core provides jQuery, not a module or theme). Declare the dependency with an extension name followed by a slash, followed by the library name, in this case core/jquery. If another other library required cuddly-slider it would declare:fluffiness/cuddly-slider, the theme name, followed by the library name. You cannot declare an individual file as a dependency, only a library.

So, to make jQuery available for cuddly-slider, we update the above to:

cuddly-slider:
  version: 1.x
  css:
    theme:
      css/cuddly-slider.css: {}
  js:
    js/cuddly-slider.js: {}
  dependencies:
    - core/jquery

Most themes will use  a global-styling asset library, for the stylesheets (CSS files) that need to be loaded on every page where the theme is active.

global-styling:
  version: 1.x
  css:
    theme:
      css/layout.css: {}
      css/style.css: {}
      css/colors.css: {}
      css/print.css: { media: print }

As you can see above, media properties like media: print can now be defined as a value at the end of a CSS asset declaration (unlike in Drupal 7, where the media property was a subkey of the stylesheets property). 

Asset loading order

As you would expect, the order in which the files are listed is the order in which they will load. By default, all JS assets are now loaded in the footer. JS for critical UI elements that cannot be shown unless their corresponding JS has run can be loaded in the header if needed like so:

js-header:
  header: true
  js:
    header.js: {}

js-footer:
  js:
    footer.js: {}

Set the header property  to true, to indicate that the JavaScript assets in that asset library are in the 'critical path' and should be loaded from the header. Any direct or indirect dependencies of libraries declared in this way will also automatically load from the header, you do not need to declare them individually for them to be available. This is the meaning of the phrase 'critical path', once an asset is declared to be in the header it is 'critical' for that asset and all of it's dependencies to load first.

Overriding and extending libraries

You must go to *.info.yml to override libraries defined in *.libraries.yml They can be either overridden or extended using libraries-override or libraries-extend. Overrides you add to *.info.yml will be inherited by sub-themes.

The stylesheets-remove property used in the *.info.yml file has been deprecated and will be removed in Drupal 9.0.x. The stylesheets-override property has already been removed.

libraries-override

The logic you will need to use when creating overrides is:

  1. Use the original module (or core) namespace for the library name.
  2. Use the path of the most recent override as the key.
  3. That path should be the full path to the file.

For example:

libraries-override:
  contextual/drupal.contextual-links:
    css:
      component:
        /core/themes/stable/css/contextual/contextual.module.css: false
    

Here contextual/drupal.contextual-links is the namespace of the core library and /core/themes/stable/css/contextual/contextual.module.css: is the full path to the most recent override of that library. In this case the file has been overridden withfalse.

It's important to note here that only the last part is an actual file system path, the others refer to namespaces. The css: and component:lines reflect the structure of the library that is being overridden.

When using this remember that reliance on the file system path means that if the file structure of your site changes, it may break this path. For that reason there is an issue to remove reliance on the full path by using stream wrappers.

Here are a few other ways to use libraries-override to remove or replace CSS or Javascript assets or entire libraries your theme has inherited from modules or themes.

libraries-override:
  # Replace an entire library.
  core/drupal.collapse: mytheme/collapse
  
  # Replace an asset with another.
  subtheme/library:
    css:
      theme:
        css/layout.css: css/my-layout.css

  # Replace a core module JavaScript asset.
  toolbar/toolbar:
    js:
      js/views/BodyVisualView.js: js/views/BodyVisualView.js

  # Remove an asset.
  drupal/dialog:
    css:
      theme:
        dialog.theme.css: false
  
  # Remove an entire library.
  core/modernizr: false

libraries-extend

libraries-extend provides a way for themes to alter the assets of a library by adding in additional theme-dependent library assets whenever a library is attached.
libraries-extend are specified by extending a library with any number of other libraries.

This is perfect for styling certain components differently in your theme, while at the same time not doing that in the global CSS. I.e. to customize the look of a component without having to load the CSS to do so on every page.

# Extend drupal.user: add assets from classy's user libraries.
libraries-extend:
  core/drupal.user: 
    - classy/user1
    - classy/user2

Attaching libraries to page(s)

Some libraries you load may not be needed on all pages. For faster performance don't load libraries where they are not being used. Below are examples of how to selectively load libraries.

Attaching a library via a Twig template

You can attach an asset library to a Twig template using the attach_library() function in any *.html.twig, file like so:

{{ attach_library('fluffiness/cuddly-slider') }}
<div>Some fluffy markup {{ message }}</div>

Attaching a library to all pages

To attach a library to all the pages that use your theme, declare it in your theme's *.info.yml file, under the libraries key:

name: Example
type: theme
core: 8.x
libraries:
  - fluffiness/cuddly-slider

List as many libraries as you want, all of them will be loaded on every page.

After editing the *.info.yml file, remember to clear the cache so that the new information is loaded into Drupal.

Attaching a library to a subset of pages

In some cases, you do not need your library to be active for all pages, but just a subset of pages. For example, you might need your library to be active only when a certain block is being shown, or when a certain node type is being displayed.

A theme can make this happen by implementing a THEME_preprocess_HOOK() function in the .theme file, replacing "THEME" with the machine name of your theme and "HOOK" by the machine name of the theme hook.

For instance, if you want to attach JavaScript to the maintenance page, the "HOOK" part is "maintenance_page", and your function would look like this:

function fluffiness_preprocess_maintenance_page(&$variables) {
  $variables['#attached']['library'][] = 'fluffiness/cuddly-slider';
}

You can do something similar for other theme hooks, and of course your function can have logic in it — for instance to detect which block is being preprocessed in the "block" hook, which node type for the "node" hook, etc.

Important note! In this case, you need to specify the cacheability metadata that corresponds to your condition! The example above works unconditionally, so no cacheability metadata is necessary. The most common use case is likely where you attach some asset library based on the current route:

function fluffiness_preprocess_page(&$variables) {
  $variables['page']['#cache']['contexts'][] = 'route';
  if (\Drupal::routeMatch()->getRouteName() === 'entity.node.preview') {
    $variables['#attached']['library'][] = 'fluffiness/node-preview';
  }
}

Attaching configurable JavaScript

In some cases, you may want to add JavaScript to a page that depends on some computed PHP information.

In this case, create a JavaScript file, define and attach a library just like before, but also attach JavaScript settings and have that JavaScript file read those settings, via drupalSettings (the successor to Drupal 7's Drupal.settings). However, to make drupalSettings available to our JavaScript file, we have to do the same work as we had to do to make jQuery available: we have to declare a dependency on it.

So that then becomes:

cuddly-slider:
  version: 1.x
  js:
    js/cuddly-slider.js: {}
  dependencies:
    - core/jquery
    - core/drupalSettings

and

function fluffiness_page_attachments_alter(&$page) {
  $page['#attached']['library'][] = 'fluffiness/cuddly-slider';
  $page['#attached']['drupalSettings']['fluffiness']['cuddlySlider']['foo'] = 'bar';
}

Where 'bar' is some calculated value. (Note that cacheability metadata is necessary here also!)

Then cuddly-slider.js will be able to access drupalSettings.fluffiness.cuddlySlider.foo (and it will === 'bar').

Adding attributes to script elements

If you want to add attributes on a script tag, you need to add an attributes key to the JSON following the script URL. Within the object following the attributes key, add the attribute name that you want to appear on the script as a new key. The value for this key will be the attribute value. If that value is set to true, the attribute will appear on its own without a value on the element.

For example:

https://maps.googleapis.com/maps/api/js?key=myownapikey&signed_in=true&libraries=drawing&callback=initMap:
  type: external
  attributes:
    defer: true
    async: true
    data-test: map-link

This would result in the following markup:

<script src="https://maps.googleapis.com/maps/api/js?key=myownapikey&signed_in=true&libraries=drawing&callback=initMap" async defer data-test="map-link"></script>

CDN / externally hosted libraries

You might want to use JavaScript that is externally on a CDN (Content Delivery Network) — e.g. web fonts are usually only available using an external URL; you can't host them yourself. This can be done by declaring the library to be external (by specifying type: external). It is also a good idea to include some information about the external library in the definition.

(Note that it is in general not a good idea to load libraries from a CDN; avoid this if possible. It introduces more points of failure both performance- and security-wise, requires more TCP/IP connections to be set up and usually is not in the browser cache anyway.)

angular.angularjs:
  remote: https://github.com/angular
  version: 1.4.4
  license:
    name: MIT
    url: https://github.com/angular/angular.js/blob/master/LICENSE
    gpl-compatible: true
  js:
    https://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js: { type: external, minified: true }

If you want your external file to be requested with the same protocol as the page is requested with, specify a protocol-relative URL:

  js:
    //ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js: { type: external, minified: true }

Or if you want to add CSS, here is an example of integrating Font Awesome:

font-awesome:
  remote: https://fortawesome.github.io/Font-Awesome/
  version: 4.5.0
  license:
    name: MIT
    url: https://fortawesome.github.io/Font-Awesome/license/
    gpl-compatible: true
  css:
    theme:
      https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css: { type: external, minified: true }

Inline JavaScript

Inline JavaScript is highly discouraged. It's recommended to put the JS you want to use inline in a file instead, because that allows JavaScript to be cached on the client side. It also allows JavaScript code to be reviewed and linted.

Inline JavaScript that generates markup

This is discouraged and generally not needed. Put the javascript in a file. Examples of this are ads, social media sharing buttons, social media listing widgets. These do use inline JavaScript. But they are just a special kind of content/markup, since they're not about decorating the site's content or making it interactive, instead they are about pulling in external content through JavaScript.

You want to put these in either a custom block or even directly in a Twig template.

E.g.:

<script type="text/javascript"><!--
ad_client_id = "some identifier"
ad_width = 160;
ad_height = 90;
//--></script>
<script type="text/javascript" src="http://adserver.com/ad.js"></script>
<a class="twitter-timeline" href="https://twitter.com/wimleers" data-widget-id="307116909013368833">Tweets by @wimleers</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>

Inline JavaScript that affects the entire page

Using any inline JavaScript is highly discouraged. Examples of inline JavaScript that affects the entire page are analytics (e.g. Google Analytics) and hosted font services. Inline JavaScript that affects the entire page can be in either of two categories: front-end/styling, or logical.

In the case of front-end/styling (e.g. hosted font services), the JS belongs in the theme. Put the JS directly in your html.html.twig file. In the case of fonts, this will also allow you to put it right in the place that gives you the best (and fastest) end user experience, because it allows you to prevent a FOUT (Flash Of Unstyled Text) while the font is still loading (fonts loaded through JS must be listed in the HTML <HEAD> before the CSS)!
(Read more about this in the excellent article “Async Typekit & the Micro-FOUT” article.)

In the other case, it belongs in the module, and for that, please see “Adding stylesheets (CSS) and JavaScript (JS) to a Drupal 8 module”.

Inline JavaScript that in an integration module

Using any inline JavaScript is highly discouraged. If you can use one of the examples above, please consider those before attempting to do this.

Two things to consider when providing a field which accepts inline JavaScript provided by a site user:

  1. The field, form or page that accepts this inline JavaScript must have a permission attached.
    Example: MODULE.routing.yml
    MODULE.settings:
      path: /admin/config/services/MODULE
      defaults:
        _title: 'MODULE settings'
        _form: \Drupal\MODULE\Form\MODULESettings
      requirements:
        _permission: 'administer site configuration'
    
  2. The value if stored in a config object should alert the render system about it's CacheableMetadata, so that when it changes the element's render cache is properly cleared/expired.
    Example: MODULES.module
    <?php
    
    /**
     * @file
     * Integrates MODULE in a Drupal site.
     */
    
    use Drupal\Core\Render\Markup;
    
    /**
     * Implements hook_page_bottom().
     */
    function MODULE_page_bottom(array &$page_bottom) {
      $settings = \Drupal::config('MODULE.settings');
      $user = \Drupal::currentUser();
      $page_bottom['MODULE'] = [
        '#markup' => Markup::create($settings->get('js_code')),
        '#cache' => [
          'contexts' => ['user'],
          'tags' => ['user:' . $user->id()],
        ],
      ];
      // Add config settings cacheability metadata.
      /** @var Drupal\Core\Render\Renderer $renderer */
      $renderer = \Drupal::service('renderer');
      $renderer->addCacheableDependency($page_bottom['MODULE'], $settings);
    }
    

Differences with Drupal 7

More information