Libraries: styles and scripts
CSS and JavaScript files in Drupal are not added with tags in a template. They are grouped into a library — a named set of files — which is then attached to whatever is being output. Drupal adds the tags to the page itself, and aggregates and compresses the files.
Style files
Create a css folder in the module and a file in it:
/* web/modules/custom/blog/css/card.css */
.blog-card {
padding: 16px;
border: 1px solid #d0d7de;
border-radius: 8px;
}On its own the file is not attached anywhere yet.
Describing a library
Module libraries are described in the file blog.libraries.yml in the root of the module folder. Create it by hand:
# web/modules/custom/blog/blog.libraries.yml
card:
css:
theme:
css/card.css: {}card is the library name. The full name is made of the module name and the library name: blog/card. File paths are written relative to the module folder. The theme key is the style level: it sets the loading order; component and theme suit component styling.
Attaching in a render array
// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
return [
'#theme' => 'blog_card',
'#title' => 'First article',
'#text' => 'A short description of the article.',
'#attached' => ['library' => ['blog/card']],
];
}After drush cr the card on the /blog page gets a border and padding. The file loads only on the pages where this element is output.
Attaching in a template
The same can be done right in the Twig template:
{# web/modules/custom/blog/templates/blog-card.html.twig #}
{{ attach_library('blog/card') }}
<article class="blog-card">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
</article>This is more convenient: the styles are tied to the template and load wherever it is used.
Scripts
Add a JavaScript file and dependencies to the library:
# web/modules/custom/blog/blog.libraries.yml
card:
css:
theme:
css/card.css: {}
js:
js/card.js: {}
dependencies:
- core/drupal
- core/onceDependencies are other libraries that must load first. core/drupal provides the Drupal object, core/once the once function.
// web/modules/custom/blog/js/card.js
(function (Drupal, once) {
Drupal.behaviors.blogCard = {
attach(context) {
once('blog-card', '.blog-card', context).forEach((card) => {
card.addEventListener('click', () => {
card.classList.toggle('is-open');
});
});
},
};
})(Drupal, once);The code is written as a behavior. Drupal calls attach when the page loads and every time new markup is added to it. The once function makes sure the handler is bound to each card only once. A click on a card now toggles the is-open class.
On every page
A library needed everywhere is attached by a hook in blog.module:
// web/modules/custom/blog/blog.module
/**
* Implements hook_page_attachments().
*/
function blog_page_attachments(array &$attachments): void
{
$attachments['#attached']['library'][] = 'blog/card';
}Common mistake
You changed the CSS and the page looks the same. There are usually two causes. The first is file aggregation: switch it off during development at Configuration → Development → Performance. The second is the cache: blog.libraries.yml is read only on drush cr. And do not add files with link and script tags in a template: Drupal does not know about them and can neither aggregate them nor load their dependencies.