Adding assets (CSS, JS) to a Drupal module via *.libraries.yml
Loading Assets
The general steps for loading assets (CSS/JS) are:
- Save the CSS or JS to a file.
- Define a "library", which can contain both CSS and JS files.
- "Attach" the library to a render array in a hook.
But in the case of themes, there is an alternative to step 3: themes can choose to load any number of asset libraries on all pages.
The following article has the following issues:
- Defining a Library
- Attaching library to Page
- Attaching library to Specific Type
- Attaching library to Render Array
- Attaching library to Render Array Of Block
- Attaching library to Form
- Attaching library to View
- Attaching library to All
- Attaching library to *.info
- Attaching library in Preprocess functions
- Attaching library in Twig template
- Attaching library during Token replacement
- Attaching library in Filter plugin
- Attaching configurable Javascript
- Adding attributes to Script elements
- Disabling Aggregation
- Externally Hosted libraries
- Load the local library first
- Inline javascript that affects page
- Dynamically Generated CSS and JS
- Hook Library Info Build
- Compare this to Drupal7
Defining a Library
To define one or more (asset) libraries, add a *.libraries.yml
file to the root of your module folder, alongside your 'my_module.info.yml' file).
file name must be according to your module name, my_module.libraries.yml
Each "library" in the file is an entry detailing CSS and JS files (assets), like this:
cuddly-slider:
version: 1.x
css:
layout:
css/cuddly-slider-layout.css: {}
theme:
css/cuddly-slider-theme.css: {}
js:
js/cuddly-slider.js: {}
You may notice the 'layout' and 'theme' keys for CSS which are not present for JS.
This indicates the style type the CSS file belongs to.
You can set CSS
weights with 5 different levels of styling:
base
: CSS reset/normalize plus HTML element styling. Key assigns a weight ofCSS_BASE
=
-
200
layout
: macro arrangement of a web page, including any grid systems. Key assigns a weight ofCSS_LAYOUT
=
-
100
component
: discrete, reusable UI elements. Key assigns a weight ofCSS_COMPONENT
=
0
state
: styles that deal with client-side changes to components. Key assigns a weight ofCSS_STATE
=
100
theme
: purely visual styling (“look-and-feel”) for a component. Key assigns a weight ofCSS_THEME
=
200
This is defined by the SMACSS standard.
So here if you specify a theme it means that the CSS file contains theme-related styling which is pure look and feel.
You cannot use other keys as these will cause strict warnings.
This example assumes that the actual JavaScript cuddly
-
slider
.
js
is located in the subfolder js
of your module.
You can also have the JS come from an external URL and include CSS files, and there are other possibilities.
See CDN / externally hosted libraries for details.
However, remember that Drupal no longer loads jQuery on all pages by default.
Drupal only loads what's necessary.
Therefore, we must declare that our module's 'my_module' library declares a dependency on the library that contains jQuery.
It is neither a module nor a theme that provides jQuery, it is Drupal core: 'core/jquery' is the dependency we want to declare.
This is an extension name followed by a slash, followed by the library name, so if some other library wanted to depend on the 'library_decleration' library, it'd have to declare a dependency on 'my_module/library_decleration' .
So, to ensure jQuery is available for js
/
cuddly
-
slider
.
js
, we update the above to:
library-decleration:
version: 1.x
css:
theme:
css/cuddly-slider.css: {}
js:
js/cuddly-slider.js: {}
dependencies:
- core/jquery
As you'd expect, the order the CSS and JS assets are listed is also the order in which they will be loaded.
By default, Drupal will attach the JS assets at the bottom of the page to avoid common issues such as DOM content loading block, code being executed that requires the presence of DOM elements that are not yet loaded, etc...
If for whatever reason, it is required to attach JS assets to the <head> section it is possible to do so using the header option:
library-decleration:
version: 1.x
header: true
js:
js/cuddly-slider.js: {}
So, now, the js/cuddly-slider.js will be attached to the page top.
Js also can have some more customization:
library-decleration:
version: 1.x
js:
js/cuddly-slider.js: {
minified: true,
attributes: { id : "script-cuddly-slider" }
}
minified
will indicate to the compiler that it's already minified and it will skip it.
If you need attributes for the script, you can add them using attributes
and put id
or any custom attribute inside.
Attaching a library to page(s)
Depending on which assets you need to have loaded, you'll want to attach the corresponding asset library differently.
After all, some asset libraries are needed on all pages, others only very rarely, and yet others on most, but not quite all.
What matters most is that we don't decide whether to attach a library based on which page we're on, i.e. which URL or route, but based on which things are visible on the page: if a page contains a '#type' => 'table', a '#type' => 'dropbutton' and a '#type' => 'foobar', then we'll only load the libraries associated with each of those '#type's.
We're not limited to '#type' only.
In case we want to load a specific asset library for only a specified instance of a '#type', you need to attach it to the render array of that instance.
You can always load libraries to all pages, but it is not recommended anymore.
The sub-sections here show examples of how to do these things.
Attaching to a Specific '#type'
(for all instances of it)
To attach a library to a specific existing '#type'
, will attach it to all instances of that type, review example:
hook_element_info_alter():
function yourmodule_element_info_alter(array &$types) {
if (isset($types['table'])) {
$types['table']['#attached']['library'][] = 'your_module/library_name';
}
}
Then clear the cache so that Drupal is aware of the new hook implementation you added.
Attaching to a render array
To attach a library to a render array (and perhaps a specific instance of a certain '#type'
), you must have access to that render array.
Perhaps you're defining the render array.
Perhaps you're modifying it in a hook.
In either case, it will look somewhat like this:
$build['the_element_that_needs_the_asset_library']['#attached']['library'][] = 'your_module/library_name';
Do not use non-numeric keys for libraries
You might want to help Drupal and not produce duplicate library entries by using non-numeric keys:
// Do NOT do this:
$build['the_element_that_needs_the_asset_library']['#attached']['library']['your_module/library_name'] = 'your_module/library_name';
The reason for this is Drupal's way of merging arrays will lead to an invalid nested array and notices like this (googlefood):
Warning: explode() expects parameter 2 to be string, array given in Drupal\Core\Asset\LibraryDependencyResolver->doGetDependencies()
Notice: Array to string conversion in system_js_settings_alter()
Notice: Array to string conversion in Drupal\Core\Asset\AttachedAssets->setLibraries()
Attaching to a render array of a Block Plugin
To give another example of attaching a library to a render array.
In cases you are building a block plugin in your module, you can attach the libraries to the render array in the build() function of your class extending the BlockBase class (as of Drupal 8 beta 6).
return [
'#theme' => 'your_module_theme_id',
'#someVariable' => $some_variable,
'#attached' => [
'library' => [
'your_module/library_name',
],
],
];
Attaching a library to a form
As forms are just rendered arrays, attaching a library works just the same:
/**
* Implements hook_form_alter().
*/
function yourmodule_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
/* @var Drupal\Core\Entity\FieldableEntityInterface $entity */
$formObject = $form_state->getFormObject();
if ($formObject instanceof \Drupal\Core\Entity\EntityFormInterface) {
$entity = $formObject->getEntity();
if (
$entity->getEntityTypeId() === 'node'
&& in_array($entity->bundle(), ['organisation', 'location', 'event', 'article'])
) {
$form['#attached']['library'][] = 'yourmodule/yourlibrary';
}
}
}
Attaching a library to a view
You can add the hook in .
module
file or .
theme
file:
// Top of the file.
use Drupal\views\ViewExecutable;
// ...
/**
* Implements hook_views_pre_render().
*/
function yourmodule_views_pre_render(ViewExecutable $view) {
if (isset($view) && ($view->storage->id() == 'super_awesome_view')) {
$view->element['#attached']['library'][] = 'custom/custom_view';
}
}
Attaching a library to all (or a subset of) pages
In some cases, the asset library is not associated with a certain part of the page, because it is associated with the entire page.
In this case, hook_page_attachments
()
exists. A clear example can be found in the Contextual Links module:
// From core/modules/contextual/contextual.module.
function contextual_page_attachments(array &$page) {
if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
return;
}
$page['#attached']['library'][] = 'contextual/drupal.contextual-links';
}
Attaching a library to all pages via *.info.yml
This is normally considered bad practice, but it is possible to attach a library to all pages via the fluffiness
.
info
.
yml
file, with this:
# Available to every page presented by the theme
libraries:
- fluffiness/cuddly-slider
Attaching a library in a preprocess function
You can attach a library in a preprocess function using the special key '#attached'
:
function yourmodule_preprocess_maintenance_page(&$variables) {
$variables['#attached']['library'][] = 'your_module/library_name';
}
Attaching a library in a twig template
You can also attach a library in a twig template by using the attach_library() twig function. So in any *.html.twig:
{{ attach_library('your_module/library_name') }}
<div>Some markup {{ message }}</div>
Attaching a library during token replacement
You can also attach a library if your custom token is present in the filtered text by adding the library to the 'BubbleableMetadata' object during replacement in hook_tokens():
/**
* Implements hook_tokens().
*/
function your_module_tokens($type, $tokens, array $data, array $options, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata) {
$replacements = [];
if ($type == 'your_module') {
foreach ($tokens as $name => $original) {
switch ($name) {
case 'your-token':
$your_render_array = your_module_build_your_renderable_thing();
$replacements[$original] = \Drupal::service('renderer')->render($your_render_array);
// LOOK HERE! WE CAN ADD LIBRARIES TOO!
$bubbleable_metadata->addAttachments(['library' => ['your_module/library_name'] ]);
break;
}
}
}
return $replacements;
}
Note that this example shows only how to do the library attachment during replacement -- to fully implement a custom token you must implement hook_token_info() as well.
Attaching a library in a filter plugin
If a module provides a text filter, then it can use the setAttachments
()
or addAttachments
()
method of the FilterProcessResult class.
For example, the filter_caption
filter does this:
if (...) { ...
$result->setProcessedText(Html::serialize($dom))
->addAttachments([
'library' => [
'filter/caption',
],
]);
}
return $result;
Attaching configurable JavaScript
In some cases, you may want to add JavaScript to a page that depends on some computed PHP information.
You can do so with drupalSettings
(the successor to Drupal 7's Drupal
.
settings
), an array of settings defined in your PHP script that can be accessed as a settings object in your JavaScript.
For using drupalSettings
in a library, we first have to declare a dependency on core
/
drupalSettings
in its library definition.
So the library definition of our previous example becomes:
cuddly-slider:
version: 1.x
js:
js/cuddly-slider.js: {}
dependencies:
- core/jquery
- core/drupalSettings
In our PHP files, we can now pass the desired drupalSettings
alongside our library.
By convention, we use our lowerCamelCase module name as the key for the settings and add the lowerCamelCase name of the library as a key.
If we'd like to pass computed values 'foo'
and 'baz'
from PHP to our example's JavaScript, we could do:
$computed_settings = [
'foo' => 'bar',
'baz' => 'qux',
];
$build['#attached']['library'][] = 'your_module/library_name';
$build['#attached']['drupalSettings']['fluffiness']['cuddlySlider'] = $computed_settings;
Then cuddly
-
slider
.
js
will be able to access drupalSettings
.
fluffiness
.
cuddlySlider
.
foo
and drupalSettings
.
fluffiness
.
cuddlySlider
.
baz
, which will have values of 'bar'
and 'qux'
respectively.
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 in 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>
Disabling Aggregation
By default, multiple local files will be aggregated where possible. To disable this for a file, set its 'preprocess' flag to false.
cuddly-slider:
version: 1.x
js:
js/cuddly-slider.js: {preprocess: false}
dependencies:
- core/jquery
- core/drupalSettings
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.
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.
However, 3rd party libraries should not be hosted on Drupal.org as part of your repo – see Policy on 3rd party libraries on Drupal.org for clarification of policy.
It does have the advantage that it's quicker to get started with, and is often claimed to load faster.
However, it also has downsides when it comes to privacy, security, and systemic risk, and may actually be slower in some common cases, as described in Reasons to avoid Javascript CDNs.
It can be done by declaring the library to be "external".
It is also a good idea to include some information about the external library in the definition.
angular.angularjs:
remote: https://github.com/angular/angular.js
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 }
Load the local library first, and use CDN as a fallback
To search for libraries in a specific order, starting with local files, and if not found using, for example, a CDN as a fallback, see Support for third-party libraries in site-specific and install profile-specific libraries folders, and JS Cookie or Tagify for examples on how to do this.
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 that JavaScript to be cached on the client side.
It also allows JavaScript code to be reviewed and linted.
Inline JS will also conflict with the Content Security Policy of many sites and make your module unusable by them.
Inline JavaScript that generates markup
This is discouraged.
Place the javascript in a file instead.
Examples of this are ads, social media sharing buttons, and 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
Inline JavaScript is highly discouraged.
Examples of inline JavaScript that affect 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.
Most of these cases can be satisfied with fixed javascript in a file plus added settings.
In the case of front-end/styling (e.g. hosted font services), it belongs in the theme, and for that, please see “Adding stylesheets (CSS) and JavaScript (JS) to a Drupal theme”.
In the other case, the JS belongs in the module. In the appropriate hook — likely hook_page_attachments
()
— define attached HTML <
HEAD
>
data by using the 'html_head'
key in the #attached
property:
function fluffiness_page_attachments(array &$attachments) {
$attachments['#attached']['html_head'][] = [
// The data.
[
'#type' => 'html_tag',
// The HTML tag to add, in this case a <script> tag.
'#tag' => 'script',
// The value of the HTML tag, here we want to end up with
// alert("Hello world!");.
'#value' => 'alert("Hello world!");',
// Set attributes like src to load a file.
'#attributes' => ['src' => ''],
],
// A key, to make it possible to recognize this HTML element when altering.
'hello-world',
];
}
Dynamically generated CSS and JS
In extremely rare and advanced cases, you may need to dynamically generate CSS and JS.
There are two categories of "dynamics":
- Dynamically built, but used across multiple requests
- Dynamically built for each request
If the dynamic CSS/JS is used across multiple requests, then you can use hook_library_info_alter
()
to modify a library to include your dynamically/automatically generated CSS/JS.
An example of Drupal core this is color_library_info_alter
()
.
Realize that just using hook_library_info_build
()
or hook_library_info_alter
()
to append a library will not automatically make the library appear in the page. You still have to define it as an attachment (either for the page or for a certain element) by using any of the techniques above.
If the dynamic CSS/JS is built for each request, then you enter the truly advanced territory.
This is hard, and for a good reason: per-request dynamic assets have to be built on every single request and therefore slow Drupal down.
We want to make it hard to make Drupal slow down, so this is why we don't offer a nice API for this — since we don't want you to do it.
It is possible though.
In the case of dynamic JS: please consider using configurable JavaScript instead, that is almost always the much better choice.
Then the logic is stored in a file (and can be reviewed, linted and cached on the client side), and only the settings to configure the logic in that file need to be generated on each request.
This can also be used for dynamic CSS: attach dynamic CSS as drupalSettings
and let some JS file add it to the page.
If using drupalSettings
plus a JavaScript file is not an option, then you still have one option left: use hook_page_attachments
()
, where you add a new value to $page
[
'#attached'
][
'html_head'
]
, which contains either a <
script
>
tag or a <
style
>
tag, as the “Inline JavaScript that affects the entire page” section above already showed.
hook_library_info_build() added for dynamic library definitions
For some advanced use cases — like detecting 3rd party libraries that need to be downloaded manually, and then exposing those as Drupal asset libraries (think Libraries API module) — you want to be able to still use PHP code to register libraries using some additional logic.
That's why hook_library_info_build
()
was added
Note that "dynamic" doesn't mean "runtime" (i.e. for every request) — that'd be terrible for performance.
The dynamically added libraries are still cached, just like libraries defined in YML files.
This means that you still need to attach the library to a page or element using any of the above techniques.
It's "dynamic" because you can use logic to control this attaching of the libraries.
Differences with Drupal 7
- In Drupal 7 libraries had to be defined using
hook_library_info
()
. That has been replaced with*.
libraries
.
yml
file. - In Drupal 8
drupal_add_css
()
,drupal_add_js
()
anddrupal_add_library
()
were removed in favor of#attached
- Now settings are only added to the page if a required library depends on the
core
/
drupalSettings
library. - Only the JavaScript required on a particular page will be added to that page. In particular, by default Drupal doesn't need JavaScript on most pages that anonymous users can see. This means that jQuery is not automatically loaded on all pages anymore.
So, if your theme requires jQuery or some other JavaScript to be present (which also is defined in an asset library), you need to tell Drupal that this is the case, by declaring a dependency on the needed asset library. - The
Drupal
.
settings
javascript object is replaced bydrupalSettings
.