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).

flowchart LR Y[meridian.libraries.yml<br/>core.libraries.yml<br/>other libraries.yml files] --> D[Library discovery<br/>hook_library_info_alter] A1[THEME.info.yml libraries] --> R[Render arrays with<br/>attached library] A2[attach_library in Twig] --> R A3[preprocess and page_attachments hooks] --> R R --> C[Asset resolver<br/>dependencies, libraries-override,<br/>libraries-extend, ordering] D --> C C --> G{Aggregation on?} G -- yes --> AG[Hashed aggregate files<br/>in public files] G -- no --> S[One tag per file] AG --> H[link and script tags in the page] S --> H

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

version

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 VERSION for libraries that follow the Drupal version.

license

Documentation only: name, url and gpl-compatible. It records the licence of bundled third-party code.

remote

Documentation only: the home of an externally hosted library (for example a project site).

header

When true, the library’s JavaScript loads in the document <head> instead of just before </body>. The dependencies of a header library are also loaded in the header. Use it only for scripts that must run before first paint (feature detection, a font loader); the default footer placement keeps pages fast.

css

CSS files grouped by SMACSS category (see next section).

js

JavaScript files, each with optional properties.

dependencies

Other libraries, in extension/name form, that must be loaded first. You can depend on libraries only, never on individual files.

deprecated

A message shown (as a deprecation notice) whenever the library is attached, used by a theme or module that retires a library: deprecated: "The %library_id% asset library is deprecated in meridian:1.2.0 and is removed in meridian:2.0.0. Use meridian/gallery instead."

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

base

Element defaults and resets: body, a, h1, form elements, no class selectors.

layout

Macro layout: page regions, grids, sidebars.

component

Discrete, reusable pieces: buttons, cards, menus, a gallery. Most files belong here.

state

Styles describing a state of a component, typically combined with a class: .is-open, .is-hidden.

theme

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 theme group file therefore still loads before a theme’s base file. A theme that must beat a module’s CSS should rely on the cascade (specificity, libraries-extend, or libraries-override), not on a group.

  • Do not fight the order with !important or with higher weights everywhere. Choose the right group, keep selectors flat and component-scoped, and use weight only 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

type

CSS, JS

file (default) or external.

attributes

CSS, JS

Attributes added to the <link> or <script> tag. true renders a bare attribute (defer: true gives defer). For example { async: true }, { crossorigin: anonymous } or an integrity hash.

preprocess

CSS, JS

Default true: the file takes part in aggregation. Set false for a file that must be served as is.

minified

CSS, JS

Default false. Tells Drupal that the file is already minified, so aggregation does not minify it again.

weight

CSS, JS

Integer ordering inside the group. For JavaScript the official documentation discourages it: use dependencies to order scripts, and use a negative value only when there is no alternative.

media

CSS

The media type or query written into the tag, for example print.

group

CSS

Rarely used override of the SMACSS group.

browsers

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

core/drupal

The Drupal global object, Drupal.behaviors, Drupal.attachBehaviors(), Drupal.t().

core/drupalSettings

The drupalSettings global that carries data from PHP.

core/once

The once() function (see below).

core/jquery

jQuery. Required only when your code uses it.

core/drupal.ajax

AJAX commands and the Drupal.ajax object, also required by anything that opens a dialog.

core/drupal.debounce

Drupal.debounce() for resize and input handlers.

core/drupal.displace

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, a NodeList or an array of elements, and returns an array of the elements that did not yet have the data-once token id. The token is stored in the data-once attribute, so the state is visible in the markup.

  • once.remove(id, selector, context) removes the token and returns the elements; call it from detach when the trigger is unload, to release timers and listeners. once.filter() and once.find() read the state without changing it.

  • detach(context, settings, trigger) runs before elements are removed (trigger is unload) 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 settings argument is the same object as the global drupalSettings. 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.

A behavior without a library is a common mistake

A script that is added to a template with a plain <script src> tag is not aggregated, not cacheable with the block, cannot be overridden and breaks a Content Security Policy that forbids inline code. Put the file in a library and attach it.

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 drupalSettings is public. It is printed in the page as JSON (a data-drupal-selector script tag of type application/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>

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;
  }
}

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/library for 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 prefer libraries-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.ajax or once will 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

libraries-extend

Survives upgrades; the file is only loaded where the component is.

Replace a stylesheet completely

libraries-override with a file replacement

Explicit, but fragile if the original path moves.

Remove a script or stylesheet nobody needs

libraries-override with false

Reduces payload; test every page that uses it.

Change a definition from a module

hook_library_info_alter()

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) and crossorigin, pin an exact version in the URL, and add the host to the site’s Content Security Policy script-src and style-src. A compromised CDN is a site-wide cross-site-scripting vector.

  • External files are never aggregated, which costs requests; preprocess has 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-face in 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: false ends the group around it.

  • The hash comes from version when 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 forgotten version is 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.yml file, 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 missing extension.libraries.yml produces 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, never DOMContentLoaded handlers that ignore AJAX-inserted content.

  • Using jQuery by habit. Without core/jquery as a dependency $ is undefined. See the next section.

  • !important and 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.yml slows 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.trim and 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/backbone or core/underscore as 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.ui in theme libraries, $(...).once() and Drupal 8-style browsers keys; 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.yml entry per cohesive piece of UI, with a version.

  • CSS under the right SMACSS group; weight only inside a group.

  • Every global used by a script is in dependencies; all behaviors use context and once().

  • Data for JavaScript goes through drupalSettings, namespaced, without secrets, with cache contexts.

  • Attach narrowly (Twig, preprocess, render array); keep THEME.info.yml libraries for the global minimum.

  • Prefer libraries-extend; use libraries-override knowingly.

  • Self-host third-party code; if external, pin the version and add integrity.

  • Aggregation on in production, off locally; ddev drush cr after every libraries edit.