Drupal: Libraries and Assets
|
This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production. This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages. |
Drupal never links a stylesheet or script by hand-writing a <link> or <script> tag in a template. Every CSS and
JavaScript file reaches a page through an asset library: a named bundle declared in a *.libraries.yml file, attached
to a page, a block or a render array, resolved with its dependencies, ordered, optionally aggregated and finally
rendered by the asset system. This page explains the complete mechanism for a themer or front-end developer: the
THEME.libraries.yml file and every key it accepts, how CSS ordering works (SMACSS groups and weights), JavaScript
dependencies, passing data with drupalSettings, Drupal behaviors and the once() helper, the several ways to attach a
library (the theme’s .info.yml, Twig, preprocess functions and render arrays), overriding and extending the libraries
of core and contributed projects, external libraries, aggregation and caching, and the current status of jQuery,
jQuery UI, Backbone and Underscore.
It was written against Drupal 11.4.x, with Drush 13 or 14 and PHP 8.3 or later, and checked against the official
documentation on 2026-10-30. The running example is the Meridian Museum and its meridian theme, generated with
Starterkit (see Starterkit and sub-themes). Drupal 12.0.0 is scheduled
for the week of 2026-12-07 and the end of life of Drupal 10 for 2026-12-09, so both are described as upcoming; no
new site should start on Drupal 10.
Why libraries exist
Drupal 7 let any module call drupal_add_css() or drupal_add_js() at any time, which made it impossible to know
what a page loaded, to cache a rendered fragment together with its assets, or to let a theme change a module’s CSS
without editing the module. Since Drupal 8 the model is declarative:
-
A library is a named set of CSS and JavaScript files, plus metadata (version, license, dependencies).
-
A library is the only unit you can attach, depend on, override or extend. You cannot attach a single file, only the library that lists it.
-
Assets are attached through render arrays (
#attached), so the render cache stores the attachment together with the markup: a cached block brings its CSS and JS with it, and a page only loads what its content needs. -
Because libraries are data, a theme can replace or remove any asset of any module without patching code.
The people involved are the themer (writes THEME.libraries.yml and attaches it), the module developer (writes
MODULE.libraries.yml for blocks, forms and widgets), the site builder (turns aggregation on, picks themes) and the
administrator (cache and security headers).
The libraries file
A theme or module declares libraries in a YAML file named after its machine name: meridian.libraries.yml at the
root of the meridian theme (or meridian_collection.libraries.yml for a module). Each top-level key is a library
name. The full identifier of a library is extension/name, for example meridian/gallery. Core libraries use the
extension name core, for example core/drupal.dialog.
# web/themes/custom/meridian/meridian.libraries.yml
global:
version: 1.0.0
license:
name: GPL-2.0-or-later
url: https://www.gnu.org/licenses/gpl-2.0.html
gpl-compatible: true
css:
base:
css/base.css: {}
layout:
css/layout.css: {}
component:
css/components/button.css: {}
css/components/card.css: {}
state:
css/state.css: {}
theme:
css/theme.css: {}
css/print.css: { media: print }
js:
js/global.js: {}
dependencies:
- core/drupal
- core/once
gallery:
version: 1.0.0
js:
js/gallery.js: { attributes: { defer: true } }
css:
component:
css/components/gallery.css: {}
dependencies:
- core/drupal
- core/drupalSettings
- core/once
- core/drupal.debounce
Paths are relative to the extension’s directory. A path that starts with / is relative to the Drupal root and is
how a library points into another extension (for example /core/themes/claro/css/components/pager.css). A URL that
starts with https:// or // is an external file and must say type: external (see below).
Library-level keys
| Key | Meaning |
|---|---|
|
A string identifying the version of the library. Since Drupal 10.1.2 it is part of the hash that names aggregate
files, and if it is omitted Drupal hashes the contents of the files instead. Change it whenever a file listed by the
library changes, otherwise browsers and CDNs can keep serving the old aggregate. Core uses the special value |
|
Documentation only: |
|
Documentation only: the home of an externally hosted library (for example a project site). |
|
When |
|
CSS files grouped by SMACSS category (see next section). |
|
JavaScript files, each with optional properties. |
|
Other libraries, in |
|
A message shown (as a deprecation notice) whenever the library is attached, used by a theme or module that retires a
library: |
CSS: SMACSS groups and weights
Drupal organises CSS after the SMACSS (Scalable and Modular Architecture for CSS) categories. The group key under
css: decides the loading order of a file, from first to last:
| Group | Intended content |
|---|---|
|
Element defaults and resets: |
|
Macro layout: page regions, grids, sidebars. |
|
Discrete, reusable pieces: buttons, cards, menus, a gallery. Most files belong here. |
|
Styles describing a state of a component, typically combined with a class: |
|
Visual decoration (colours, fonts, shadows). Core and contributed code put "skin" styles here so that a theme can override them. |
Inside a group, files load in the order they are written, and the optional weight property (an integer from -50 to
+50, default 0) moves a file earlier (lower) or later (higher). Two rules from the official documentation are
important:
-
Modules' styles always load before themes' styles, and the SMACSS groups cannot be interlaced between modules and themes. A module’s
themegroup file therefore still loads before a theme’sbasefile. A theme that must beat a module’s CSS should rely on the cascade (specificity,libraries-extend, orlibraries-override), not on a group. -
Do not fight the order with
!importantor with higher weights everywhere. Choose the right group, keep selectors flat and component-scoped, and useweightonly to order two files inside one group.
The group property exists for an individual file but is rarely needed because the SMACSS key already places it.
# Two files in the component group with an explicit order
card-grid:
version: 1.0.0
css:
component:
css/components/card.css: { weight: -1 }
css/components/card-grid.css: {}
Properties of a CSS or JavaScript file
Each file is a key whose value is a map of properties (use {} when there are none).
| Property | Applies to | Meaning |
|---|---|---|
|
CSS, JS |
|
|
CSS, JS |
Attributes added to the |
|
CSS, JS |
Default |
|
CSS, JS |
Default |
|
CSS, JS |
Integer ordering inside the group. For JavaScript the official documentation discourages it: use |
|
CSS |
The media type or query written into the tag, for example |
|
CSS |
Rarely used override of the SMACSS group. |
|
CSS, JS |
Deprecated in 9.1.0 and removed in 10.0.0 (it generated Internet Explorer conditional comments). Old tutorials and Drupal 8 themes still show it. |
Two more removals matter when you read older material: stylesheets-override was removed and stylesheets-remove is
deprecated. Their replacement is libraries-override, described below.
Dependencies and JavaScript loading
JavaScript loads in the footer, after its dependencies, in an order Drupal computes from the dependency graph. That is
why Drupal asks you to declare dependencies instead of weights: the resolver topologically sorts libraries so that
core/drupal loads before anything that uses Drupal.behaviors.
The core libraries a theme most often depends on are:
| Library | Provides |
|---|---|
|
The |
|
The |
|
The |
|
jQuery. Required only when your code uses it. |
|
AJAX commands and the |
|
|
|
Offsets caused by sticky toolbars, useful for fixed headers. |
A library only gets its dependencies, not the dependencies of the page: if your script uses Drupal.t() and you did
not list core/drupal, it works when another library happened to load it and fails on the page where nothing else did.
Always list every global your file touches.
Drupal behaviors and once()
Drupal’s JavaScript API is built on behaviors. A behavior is an object with an attach function (and, optionally, a
detach function) registered on Drupal.behaviors. Drupal calls every attach(context, settings) once on page load
with document as the context, and again each time new markup arrives (an AJAX response, a dialog, a form rebuild, a
Views refresh) with the new DOM fragment as the context. A script that searches the whole document on every call, or
that binds a handler twice on the same element, is the usual source of duplicated listeners and slow pages.
The once() function from the core/once library solves the second problem: it returns only the elements that have
not yet been processed under a given name and marks them as processed.
// web/themes/custom/meridian/js/gallery.js
((Drupal, drupalSettings, once) => {
Drupal.behaviors.meridianGallery = {
attach(context, settings) {
const galleries = once('meridian-gallery', '.exhibition-gallery', context);
galleries.forEach((gallery) => {
const { autoplay, interval } = settings.meridian.gallery;
const slides = gallery.querySelectorAll('.exhibition-gallery__slide');
let index = 0;
const show = (i) => {
slides.forEach((slide, n) => slide.toggleAttribute('hidden', n !== i));
index = i;
};
show(0);
gallery.querySelector('.exhibition-gallery__next')
?.addEventListener('click', () => show((index + 1) % slides.length));
if (autoplay) {
gallery.dataset.timer = window.setInterval(
() => show((index + 1) % slides.length),
interval,
);
}
});
},
detach(context, settings, trigger) {
if (trigger === 'unload') {
once.remove('meridian-gallery', '.exhibition-gallery', context).forEach((gallery) => {
window.clearInterval(Number(gallery.dataset.timer));
});
}
},
};
})(Drupal, drupalSettings, once);
Source: JavaScript API overview (the page redirects to a new documentation host that does not resolve at the time of writing; the canonical path above is the one to cite)
How it works:
-
once(id, selector, context)accepts a CSS selector, a single element, aNodeListor an array of elements, and returns an array of the elements that did not yet have thedata-oncetokenid. The token is stored in thedata-onceattribute, so the state is visible in the markup. -
once.remove(id, selector, context)removes the token and returns the elements; call it fromdetachwhen the trigger isunload, to release timers and listeners.once.filter()andonce.find()read the state without changing it. -
detach(context, settings, trigger)runs before elements are removed (triggerisunload) or before a form is serialised for an AJAX submit (serialize). A behavior that creates timers, observers or global listeners must clean them up there. -
The
settingsargument is the same object as the globaldrupalSettings. Prefer using the argument inside the behavior and passing it to helpers. -
Name behaviors with the extension’s machine name and a descriptive suffix (
meridianGallery): the name is global across all code on the page, and a clash silently replaces another extension’s behavior.
Older code: jQuery Once
Older tutorials and the book Drupal 10 Development Cookbook (2023) era snippets use $(selector).once('name') and
the core/jquery.once library. That plugin was deprecated in 9.3 and removed in 10.0, replaced by core/once. A theme
carried over from Drupal 8 or 9 that still depends on core/jquery.once fails to attach the library on Drupal 10 and
11, and a script that calls .once() throws $(...).once is not a function. Replace the dependency with
core/once, and the call with the function form shown above. Likewise, $.fn.once should not be reintroduced by a
polyfill; the function form works without jQuery.
Modernizing Drupal 10 Theme Development (2023) shows once with a jQuery wrapper ($(once('x', '.y', context))). That
still works, but only because once() returns an array that jQuery can wrap; new code does not need jQuery.
Passing data with drupalSettings
Data computed in PHP reaches JavaScript through the drupalSettings object. The library that provides it is
core/drupalSettings; list it as a dependency of any library that reads it. PHP side, attach the values to a render
array under #attached:
<?php
/**
* @file
* Theme functions for the meridian theme.
*/
/**
* Implements hook_preprocess_HOOK() for node templates.
*/
function meridian_preprocess_node(array &$variables): void {
if ($variables['node']->bundle() !== 'exhibition' || $variables['view_mode'] !== 'full') {
return;
}
$variables['#attached']['library'][] = 'meridian/gallery';
$variables['#attached']['drupalSettings']['meridian']['gallery'] = [
'autoplay' => FALSE,
'interval' => 6000,
'nid' => (int) $variables['node']->id(),
];
}
Points to remember:
-
Everything in
drupalSettingsis public. It is printed in the page as JSON (adata-drupal-selectorscript tag of typeapplication/json). Never put secrets, API keys, internal URLs, private identifiers or personal data in it. -
Values from several sources are merged recursively by key, so use a namespace named after your extension (
meridian.gallery) to avoid collisions. Arrays are merged by index, which can surprise you when two attachments both add list items. -
The values are part of the render array and therefore part of the cache entry. If the value depends on the current user, route, language or query string, add the matching cache context (see Render API and cache metadata). A missing context is the main reason one visitor sees another visitor’s settings.
-
To change settings late (for example in a module that must adjust another module’s values), implement
hook_js_settings_alter(). -
From a block or controller the same key works on any render array:
$build['#attached'['drupalSettings']['meridian']['map']['zoom'\] = 12;].
Attaching libraries
A library does nothing until it is attached. There are five ways, from the broadest to the narrowest scope.
In THEME.info.yml (every page)
# meridian.info.yml
name: Meridian
type: theme
base theme: false
core_version_requirement: ^11.2
libraries:
- meridian/global
The libraries key attaches the listed libraries on every page where the theme is active. Keep it small: it is the
critical path of every request.
In a Twig template (one template)
{# templates/content/node--exhibition--full.html.twig #}
{{ attach_library('meridian/gallery') }}
<article{{ attributes.addClass('exhibition') }}>
{{ content }}
</article>
Source: Functions in Twig templates
attach_library() attaches the library whenever the template is rendered, and, because the attachment lives in the
render array, the library is stored with the cached output of that template. It is the right tool for a template that
needs its own CSS or JS: a component loaded only on pages that use it. The argument must be the literal
extension/name string; it does not accept a variable built from user input. Single Directory Components do this
automatically for their own CSS and JS (see Single Directory
Components); you do not write a library entry for a component.
In a preprocess function (conditionally, for a template or entity)
Use hook_preprocess_HOOK() in the theme’s .theme file (or in a class with #[Hook] from Drupal 11.3, see
Hooks and OOP hooks) when attaching depends on data: the bundle, the view mode,
a field value, the route. The meridian_preprocess_node() function above is an example. See also
Template suggestions and preprocess.
In page-level hooks
hook_page_attachments() and hook_page_attachments_alter() receive the page attachments array and can add
libraries or settings for the whole page, for example by route:
function meridian_page_attachments_alter(array &$attachments): void {
$attachments['#attached']['library'][] = 'meridian/analytics';
$attachments['#cache']['contexts'][] = 'route';
}
Anything added conditionally must also carry the cache metadata that explains the condition, otherwise the page cache serves the wrong assets to the wrong pages.
In a render array (blocks, controllers, forms, formatters)
Modules attach their libraries in code, for example in the build() method of the block "On display now":
public function build(): array {
return [
'#theme' => 'item_list',
'#items' => $this->loadObjects(),
'#attached' => [
'library' => ['meridian_collection/on-display'],
'drupalSettings' => ['meridianCollection' => ['refresh' => 60]],
],
'#cache' => ['tags' => ['collection_object_list'], 'max-age' => 3600],
];
}
To attach to a form element use $form['#attached']['library'][]. For a Views display, add the library in a
preprocess function for the view template, not in the View’s "Header" area.
Dynamic library definitions
A module that must generate library definitions at runtime (for example one library per configured map style)
implements hook_library_info_build(); the result is cached with the other library data. A module or theme that must
change an existing definition implements hook_library_info_alter(array &$libraries, string $extension):
function meridian_library_info_alter(array &$libraries, string $extension): void {
if ($extension === 'core' && isset($libraries['drupal.dialog'])) {
// Move a core file to the end of its group.
$libraries['drupal.dialog']['css']['component']['misc/dialog/dialog.css']['weight'] = 10;
}
}
Source: hook_library_info_alter
Prefer libraries-override and libraries-extend in .info.yml when you can: they are data, easy to read and removed
with the theme.
Overriding and extending libraries
Both keys live in THEME.info.yml and are inherited by sub-themes.
libraries-override
# meridian.info.yml
libraries-override:
# Replace a whole library with one of the theme's own.
core/drupal.collapse: meridian/collapse
# Replace one file inside a library.
core/drupal.dialog:
css:
component:
misc/dialog/dialog.css: css/overrides/dialog.css
# Remove one file.
system/base:
css:
component:
css/components/hidden.module.css: false
# Remove a whole library (use sparingly).
core/modernizr: false
-
The key is
extension/library-name; a file key must match the path written in the original library, including the SMACSS group. -
The replacement is a path inside the theme or another
extension/libraryfor a whole library. -
If a parent theme (for example
stable9) has already overridden a file, a sub-theme must use the original library name but the full path of the latest override as the key. Paths break when the parent moves its files, which is one reason to preferlibraries-extend. -
Removing a core library can break other code that depends on it; the assets are removed from the page and a script that expects
Drupal.ajaxoroncewill fail with a console error, not with a Drupal message.
libraries-extend
libraries-extend adds extra libraries whenever a given library is attached, so you can style a core or module
component without loading your CSS on pages that do not use it:
libraries-extend:
core/drupal.dialog:
- meridian/dialog-skin
core/drupal.tabledrag:
- meridian/tabledrag-skin
views/views.module:
- meridian/exhibition-lists
Which to choose
| Goal | Use | Why |
|---|---|---|
Add styling to a core component |
|
Survives upgrades; the file is only loaded where the component is. |
Replace a stylesheet completely |
|
Explicit, but fragile if the original path moves. |
Remove a script or stylesheet nobody needs |
|
Reduces payload; test every page that uses it. |
Change a definition from a module |
|
The only option available to a module. |
External libraries
A library can load a file hosted elsewhere. Declare it with type: external:
meridian_map:
remote: https://example.com/maps
version: 3.2.1
license:
name: BSD-2-Clause
url: https://example.com/maps/LICENSE
gpl-compatible: true
js:
https://cdn.example.com/maps/3.2.1/maps.min.js:
type: external
minified: true
attributes:
crossorigin: anonymous
integrity: sha384-CHANGE_ME
css:
component:
https://cdn.example.com/maps/3.2.1/maps.min.css: { type: external, minified: true }
Protocol-relative URLs (//host/path) are accepted but no longer recommended. The official documentation discourages
loading code from a third-party host and says third-party code should not be committed into a Drupal.org project. For
a Meridian-style production site:
-
Prefer local copies. Install the library with npm and copy or build it into
js/vendor/of the theme (see Front-end tooling). A local file works offline, is aggregated, needs no extra connection and does not leak the visitor’s IP address to a third party. -
If you must use a CDN, add
integrity(Subresource Integrity) andcrossorigin, pin an exact version in the URL, and add the host to the site’s Content Security Policyscript-srcandstyle-src. A compromised CDN is a site-wide cross-site-scripting vector. -
External files are never aggregated, which costs requests;
preprocesshas no effect on them. -
Fonts and map tiles are a privacy question in the European Union: loading Google Fonts from the vendor’s host sends visitors' addresses to the provider without consent. Self-host the font files and declare them with
@font-facein the theme’s CSS.
Third-party scripts can also be wrapped as small libraries, with attributes such as { async: true }. Inline
JavaScript is strongly discouraged: it cannot be aggregated, cached or covered by a strict Content Security Policy.
When it is unavoidable (a font loader, a critical snippet), place it in html.html.twig; for user-provided inline
code, require a permission and add cacheable dependencies so it is invalidated correctly.
Aggregation and caching
When aggregation is on, Drupal combines the CSS and JavaScript files of a page into a small number of files in
public://css and public://js, with names derived from a hash. The switch lives at Administration > Configuration
> Development > Performance (/admin/config/development/performance) as "Aggregate CSS files" and "Aggregate
JavaScript files", and in configuration as system.performance:css.preprocess and system.performance:js.preprocess.
-
Aggregation happens by grouping files that share media and attributes, in the order the resolver produced. A file with
preprocess: falseends the group around it. -
The hash comes from
versionwhen present (Drupal 10.1.2 and later), from file contents otherwise. A file edited without a version bump keeps the old name until the cache is cleared, which is why a forgottenversionis a common cause of "my change does not appear in production". -
Aggregates are written under the public files directory, so the directory must be writable on the web server, and with several web servers the aggregates should be on shared storage or be rebuilt on each (see Performance and caching).
-
On a development machine, turn aggregation off and enable Twig debugging so you see every file; settings files for local environments usually override the performance config:
// web/sites/default/settings.local.php
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
-
Library definitions are cached. After editing a
*.libraries.ymlfile, rebuild the cache:ddev drush cr. A new library name, a changed dependency or a new file is not visible until then. -
With HTTP/2 or HTTP/3 several small files cost less than before, but aggregation is still the default on production because it also lets the CDN cache fewer objects.
# Inspect what Drupal knows about a library
ddev drush php:eval "print_r(\Drupal::service('library.discovery')->getLibraryByName('meridian', 'gallery'));"
# Rebuild after changing a libraries file
ddev drush cr
Common errors and pitfalls
-
Library not attached or not found. A typo in
meridian/galery, a library file named after a different machine name, or a missingextension.libraries.ymlproduces no assets and, depending on the call, a log message. Check Reports > Recent log messages and rebuild the cache. -
Attaching with a template or preprocess function but forgetting the cache metadata. The render cache then serves a page without the assets (or with them everywhere).
-
Scripts that run before the DOM is ready or run twice. Use behaviors,
once()and the context argument, neverDOMContentLoadedhandlers that ignore AJAX-inserted content. -
Using jQuery by habit. Without
core/jqueryas a dependency$is undefined. See the next section. -
!importantand absolute paths in CSS. Use relative URLs for images and fonts: they are rewritten correctly inside aggregates, while absolute/themes/...paths break on sites installed in a subdirectory. -
Large global libraries. Attaching every component stylesheet in
THEME.info.ymlslows each page. Split by component and attach where used (Twig, preprocess or SDC). -
Overriding with a path key that no longer exists. Drupal reports an invalid override specification when a theme overrides something the library does not contain.
jQuery, jQuery UI, Backbone and Underscore
-
jQuery is optional. Drupal no longer loads jQuery on every page; a library that needs it declares
core/jquery. Core JavaScript has been moving to plain JavaScript for years, and a new theme does not need jQuery at all. -
jQuery 4 in the 11 line. The change record "jQuery updated to 4.0.0-beta" states that Drupal 11 ships with jQuery 4, which removes APIs deprecated in jQuery 3 (for example
jQuery.type,jQuery.isArray,jQuery.trimand the shorthand event methods). Code written for jQuery 3 that avoided deprecated calls keeps working, and the contributed ecosystem has been fixing libraries (Bootstrap and carousels such as Slick) for it. Test every third-party jQuery plugin before moving a site from Drupal 10 to 11. -
jQuery UI is internal. Core still carries jQuery UI (1.14.2 in the 12.0.0-beta1 notes) for its own use, but it is no longer public API for themes and modules: the individual jQuery UI libraries were deprecated in 8.8 and 9.2 and replaced by contributed modules such as the jQuery UI project and its widget projects. If a theme still depends on
core/jquery.ui.*, install the contributed module that provides the widget you use, or better, replace the widget with native HTML (<dialog>,<details>) or a maintained library. -
Backbone and Underscore are gone. They have not been offered as libraries by core since Drupal 10, and the 12.0.0-beta1 release notes state that the third-party Backbone and Underscore libraries have been removed. A theme or module that bundled its own copy keeps working; one that listed
core/backboneorcore/underscoreas a dependency must ship its own copy or rewrite. -
Older books such as Drupal 10 Development Cookbook (2023) and Modernizing Drupal 10 Theme Development (2023) show
core/jquery.uiin theme libraries,$(...).once()and Drupal 8-stylebrowserskeys; none of that applies to Drupal 10.0 and later, and the first two are gone for good in Drupal 12.
// Same behavior without jQuery
((Drupal, once) => {
Drupal.behaviors.meridianOpeningHours = {
attach(context) {
once('opening-hours', '[data-opening-hours-toggle]', context).forEach((button) => {
button.addEventListener('click', () => {
const panel = document.getElementById(button.getAttribute('aria-controls'));
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!expanded));
panel.toggleAttribute('hidden', expanded);
});
});
},
};
})(Drupal, once);
Contributed projects report different minor versions in which jQuery 4 compatibility work landed, so confirm the exact
jQuery version your site ships by reading core/core.libraries.yml (the jquery library) in the installed core.
Libraries and Single Directory Components
An SDC (see Single Directory Components) is a folder with its own
Twig, CSS and JavaScript files. Core generates an anonymous library for each component and attaches it when the
component renders, so a component’s css and js do not appear in THEME.libraries.yml. Use the libraries file
for what is shared by many pages (reset, typography, layout, global behaviors) and for third-party code, and use
components for self-contained pieces such as card, hero, exhibition-teaser and opening-hours.
Libraries and Drupal 12
In Drupal 12, Claro and Olivero are removed from core (the Default Admin theme, based on Gin, replaces the admin
theme), so any libraries-override or libraries-extend keyed on claro/... or olivero/... stops applying, and
Stable 9 (deprecated in 11.4) is removed too. Backbone and Underscore are removed as described above. A theme that
only uses core/drupal, core/once, core/drupalSettings and its own files is unaffected. Run
phpstan with the drupal-check/deprecation rules and search the theme for jquery.once, jquery.ui, backbone,
underscore, stable9 and classy before upgrading (see
Preparing for Drupal 12).
Checklist
-
One
THEME.libraries.ymlentry per cohesive piece of UI, with aversion. -
CSS under the right SMACSS group;
weightonly inside a group. -
Every global used by a script is in
dependencies; all behaviors usecontextandonce(). -
Data for JavaScript goes through
drupalSettings, namespaced, without secrets, with cache contexts. -
Attach narrowly (Twig, preprocess, render array); keep
THEME.info.ymllibrariesfor the global minimum. -
Prefer
libraries-extend; uselibraries-overrideknowingly. -
Self-host third-party code; if external, pin the version and add
integrity. -
Aggregation on in production, off locally;
ddev drush crafter every libraries edit.