Asset libraries
Drupal never loads a CSS or JavaScript file directly; it loads libraries. A library names a set of assets plus their dependencies, which lets Drupal load only what each page needs, order files by dependency instead of luck, and aggregate them for production.
Define your own libraries, attach them at the right scope (everywhere, per template, or per render array), and change what other modules and themes load without forking their code. For component-scoped assets, single-directory components generate their library automatically; this page covers everything else.
Prerequisites
Section titled “Prerequisites”- A custom module or theme, for example from the first theme tutorial.
- For the JavaScript side specifically, read JavaScript behaviors next; this page stops at getting files onto the page.
Define a library
Section titled “Define a library”Libraries live in EXTENSION.libraries.yml next to the info file. The parser for every key is \Drupal\Core\Asset\LibraryDiscoveryParser; this module example uses the common ones:
# Every top-level key is one library. Other code refers to it as# "docs_snippets/reading_time" (extension name / library name).reading_time: version: 1.0.0 css: # CSS files are grouped by SMACSS category: base, layout, component, # state, or theme. The category decides the aggregation group and # loading order, so pick the one that matches what the CSS does. component: css/theming_howtos_reading_time.css: {} js: # JS loads in the footer by default; set "header: true" on the library # to move it (and all its dependencies) to the <head>. js/theming_howtos_reading_time.js: {} dependencies: # Drupal.behaviors and once() each live in their own core library. # core/drupal also pulls in the drupalSettings bridge. - core/drupal - core/onceThe keys that matter:
cssgroups files by SMACSS category (Scalable and Modular Architecture for CSS):base,layout,component,state, ortheme. The category translates into a weight, sobaseCSS reliably loads beforethemeCSS regardless of which libraries are on the page.jslists files in order. Positive weights are rejected outright; if you need to load after something, depend on it instead.dependencieslists other libraries asextension/library. Dependencies are loaded first, and they are the only ordering tool you should reach for.versionfeeds cache-busting query strings;VERSIONmeans the current core version.- Asset paths are relative to the extension. A leading
/makes them relative to the Drupal root,//or a full URL marks them external, and/libraries/...paths are resolved through the site’s libraries directories.
A theme’s library looks identical:
# An asset library bundles CSS and JS so Drupal can attach, aggregate and# cache them. "global" is attached site-wide via first_theme.info.yml.global: css: # The "base" / "layout" / "component" / "theme" categories control # aggregation order (SMACSS). base: css/base.css: {} js: js/first-theme.js: {} dependencies: - core/drupal - core/onceAttach the library
Section titled “Attach the library”How you attach decides when the assets load. From narrowest to widest:
From a render array, for assets only one element needs. Cacheable: the attachment travels with the element through render caching.
// Fragment: inside any render array build.$build['#attached']['library'][] = 'docs_snippets/reading_time';From a Twig template, for assets tied to one template’s markup:
{{ attach_library('first_theme/slider') }}From a hook, for assets with conditions a render array cannot express. hook_page_attachments() runs for every page, so keep the logic cheap:
/** * Implements hook_page_attachments(). * * Attaches an asset library on every page, and passes a PHP value to * JavaScript through drupalSettings. The settings only reach the browser * because the attached library depends on core/drupal, which loads the * drupalSettings bridge. */#[Hook('page_attachments')]public function pageAttachments(array &$attachments): void { $attachments['#attached']['library'][] = 'docs_snippets/reading_time'; $attachments['#attached']['drupalSettings']['docsSnippets']['readingTime'] = [ 'wordsPerMinute' => 225, ];}When the attachment depends on the request or the user, add the cache context it varies by to $attachments. Without it, the page caches store the first variant and serve it to everyone:
// Fragment: attach only on node pages, and vary the cached page by route.if ($this->routeMatch->getRouteName() === 'entity.node.canonical') { $attachments['#attached']['library'][] = 'docs_snippets/reading_time';}$attachments['#cache']['contexts'][] = 'route';Globally from a theme’s info file, for the theme’s base stylesheet and similar always-on assets. The libraries key in first_theme.info.yml:
name: First themetype: themedescription: 'A standalone (starterkit-style) theme built in the "Build your first theme" tutorial.'package: Documentation examplescore_version_requirement: ^11# Standalone themes own all of their markup and CSS. Generate one with:# php core/scripts/drupal generate-theme first_theme'base theme': falselogo: logo.svg
libraries: # Attached on every page. Defined in first_theme.libraries.yml. - first_theme/global - core/normalize
regions: header: Header primary_menu: 'Primary menu' breadcrumb: Breadcrumb highlighted: Highlighted content: Content sidebar: Sidebar footer: FooterPrefer the narrowest scope that works. A library attached in a render array costs nothing on pages that never render the element; a global library taxes every page forever.
Override and extend other extensions’ libraries
Section titled “Override and extend other extensions’ libraries”Themes get two declarative keys in their info file. libraries-override replaces or removes assets (or whole libraries) whenever they would load:
libraries-override: system/base: css: component: css/components/hidden.module.css: css/my-hidden.cssSetting a file or a whole library to false removes it. Asset paths on the left side must match the original definition exactly; the replacement path is relative to your theme.
libraries-extend loads your library every time another library loads, which is the right way to ship CSS for another module’s widget:
libraries-extend: core/drupal.dialog: - first_theme/dialog-stylesModules do not have info-file keys; they alter definitions in code with hook_library_info_alter():
/** * Implements hook_library_info_alter(). * * Changes another extension's library definition at build time. Themes * can use libraries-extend and libraries-override in their info file; * modules use this hook. */#[Hook('library_info_alter')]public function libraryInfoAlter(array &$libraries, string $extension): void { // Load this module's assets whenever core's dialog library loads. if ($extension === 'core' && isset($libraries['drupal.dialog'])) { $libraries['drupal.dialog']['dependencies'][] = 'docs_snippets/reading_time'; }}One thing neither mechanism can do: drupalSettings defaults cannot be overridden through libraries-override (the parser throws an exception); use hook_library_info_alter() for that.
Define libraries dynamically
Section titled “Define libraries dynamically”When a library’s contents depend on runtime state (a setting, an optional module), implement hook_library_info_build() and return library definitions built in PHP. Two properties of this hook surprise people:
- It defines libraries for your module only. Drupal invokes the single implementation belonging to the extension whose libraries are being parsed; you cannot inject libraries into another extension from it (that is
hook_library_info_alter()’s job). - Static libraries do not belong in it. If the definition never changes, keep it in
*.libraries.ymlwhere it is discoverable.
Let aggregation work for you
Section titled “Let aggregation work for you”With CSS and JavaScript aggregation enabled (the default; Administer → Configuration → Development → Performance, /admin/config/development/performance), Drupal concatenates assets into a small number of aggregate files per page. You normally change nothing for this to work, but it explains two rules above:
- Files are grouped before aggregation, and theme CSS aggregates separately from module CSS, so SMACSS categories and dependencies survive aggregation while ad-hoc ordering tricks do not.
- Each distinct set of libraries produces distinct aggregates, which is another reason to attach narrowly: a library attached globally invalidates every page’s aggregate when it changes.
Mark already-minified files with minified: true so Drupal does not reprocess them.
Verify
Section titled “Verify”- Run
ddev drush cache:rebuild(ddev drush cr); library definitions are cached. - Load a page where the library should be attached and one where it should not.
- With aggregation off (uncheck both boxes on the Performance page during development), the page source lists each file in a
linkorscripttag; confirm your files appear on the first page and not the second. - A library that is defined but produces a YAML error or a missing file fails loudly: an
IncompleteLibraryDefinitionExceptionor a 404 in the network panel tells you which side is wrong.
Variations
Section titled “Variations”External and third-party assets
Section titled “External and third-party assets”Files loaded from a CDN use type: external (or a full URL as the file key). Third-party files copied into your project must declare remote and license keys, and the parser enforces the pair: a remote without license throws an exception.
JavaScript in the header
Section titled “JavaScript in the header”JS loads in the footer by default. Set header: true on the library to move it and its dependencies into the head element, for scripts that must run before rendering. Treat it as a last resort; footer scripts do not block first paint.
Per-file attributes
Section titled “Per-file attributes”Any attribute you can put on a script tag can ride along: js/analytics.js: { attributes: { defer: true } }.
Related
Section titled “Related”- JavaScript behaviors covers what goes inside the JS files you attach.
- Build your first theme 3: libraries, CSS, and breakpoints builds a theme’s library set from scratch.
- Single-directory components get an auto-generated library per component.
The library.discovery serviceis the runtime API for inspecting parsed definitions.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.