Skip to content
HELiX

Library System Deep Dive

apps/docs/src/content/docs/drupal/library-system Click to copy
Copied! apps/docs/src/content/docs/drupal/library-system

Drupal’s asset library system is the foundation for loading and managing HELiX web components in your Drupal site. This deep dive covers the complete architecture, from basic library definitions to advanced optimization strategies and tree-shaking patterns.

Drupal’s library system provides a declarative way to define, version, and load CSS and JavaScript assets. Libraries are defined in YAML files and attached to pages through themes, modules, or render arrays. This system enables:

  • Dependency management — Automatic loading of required libraries
  • Version control — Cache-busting and compatibility tracking
  • Aggregation — Combining files for production performance
  • Conditional loading — Load assets only when needed
  • Weight ordering — Control execution order of scripts

For HELiX web components, the library system handles:

  1. Loading ES modules with proper MIME types
  2. Managing dependencies between components
  3. Enabling tree-shaking for optimal bundle sizes
  4. Providing version control for component updates
  5. Supporting both CDN and npm distribution strategies

Every Drupal theme and module can define libraries in a THEMENAME.libraries.yml or MODULENAME.libraries.yml file in the theme/module root directory.

library-name:
version: 1.0.0
js:
path/to/file.js: {}
css:
theme:
path/to/file.css: {}
dependencies:
- core/drupal

Key components:

  • Library name — Unique identifier within the theme/module namespace
  • Version — Used for cache-busting and dependency tracking
  • js — JavaScript file definitions
  • css — CSS file definitions (categorized by type: base, layout, theme, component, state)
  • dependencies — Other libraries that must load first

File paths in .libraries.yml are relative to the theme/module root:

helix-button:
js:
# Relative to theme root: themes/custom/mytheme/dist/js/hx-button.js
dist/js/hx-button.js: {}

For external URLs (CDN):

helix-cdn:
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/core.js:
type: external

Basic JavaScript file:

library-name:
js:
js/script.js: {}

With options:

library-name:
js:
js/script.js:
minified: true
preprocess: false
attributes:
defer: true

ES Module (required for HELiX components):

helix-component:
js:
dist/js/hx-button.js:
attributes:
type: module

Critical: Web components built with Lit require type: module to load as ES modules. Without this attribute, the browser will fail to parse import statements.

CSS files are categorized by purpose:

library-name:
css:
base:
css/reset.css: {}
layout:
css/grid.css: {}
component:
css/button.css: {}
theme:
css/colors.css: {}
state:
css/print.css: { media: print }

Categories determine load order:

  1. base — Resets, normalizers
  2. layout — Grid systems, structural CSS
  3. component — Component-specific styles
  4. theme — Visual styling, colors, typography
  5. state — Print styles, accessibility overrides

For CDN-hosted files:

helix-cdn:
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/core.js:
type: external
attributes:
type: module
crossorigin: anonymous

External asset options:

  • type: external — Required for all external URLs
  • minified: true — Marks file as already minified (skip aggregation)
  • attributes — HTML attributes for the <script> or <link> tag

Dependencies ensure libraries load in the correct order.

helix-button:
js:
dist/js/hx-button.js:
attributes:
type: module
dependencies:
- core/once
- mytheme/helix-core

Syntax: namespace/library-name

  • core/* — Drupal core libraries
  • themename/* — Theme libraries
  • modulename/* — Module libraries

Drupal resolves dependencies recursively:

mytheme.libraries.yml
helix-core:
js:
dist/js/helix-core.js:
attributes:
type: module
helix-button:
js:
dist/js/hx-button.js:
attributes:
type: module
dependencies:
- mytheme/helix-core
helix-form:
js:
dist/js/hx-form.js:
attributes:
type: module
dependencies:
- mytheme/helix-button # Transitively loads helix-core

Load order when helix-form is attached:

  1. helix-core.js
  2. hx-button.js
  3. hx-form.js

Useful Drupal core libraries for HELiX integration:

helix-behaviors:
js:
js/helix-behaviors.js: {}
dependencies:
- core/drupal # Drupal global object
- core/drupalSettings # Settings from PHP
- core/once # Run-once utility
- core/jquery # jQuery (if needed)

Best practice: Minimize dependencies on core/jquery. Modern web components don’t require jQuery.

helix-button:
version: 3.9.0
js:
dist/js/hx-button.js:
attributes:
type: module

Version affects:

  • Cache-busting query strings (hx-button.js?v=3.9.0)
  • Cache and dependency-graph metadata (the version contributes to cache hashes and library replacement keys; Drupal’s libraries API does not perform semver dependency-compatibility checks)
  • Library replacement (modules can replace libraries with specific versions)

Drupal’s Libraries API substitutes the VERSION placeholder with the Drupal core version (see drupal.org docs on libraries.yml), not the theme/module version. Use it when the asset’s cache-bust should track Drupal core; for theme-scoped versioning, write the literal version string yourself:

helix-button:
version: VERSION # Substituted with Drupal core version (e.g. 10.4.0)
js:
dist/components/hx-button/index.js:
attributes:
type: module
helix-button-theme-versioned:
version: '3.9.0' # Literal — track this library's version explicitly
js:
dist/components/hx-button/index.js:
attributes:
type: module

For CDN assets with their own versioning:

helix-cdn:
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/core.js:
type: external
version: -1 # Disable query string
attributes:
type: module

Control execution order with weight:

helix-init:
js:
js/helix-init.js:
weight: -10 # Load early
attributes:
type: module
helix-behaviors:
js:
js/helix-behaviors.js:
weight: 0 # Default weight
dependencies:
- mytheme/helix-init

Weight conventions: Drupal accepts any integer (positive or negative). Negative weights load earlier; positive weights load later. The exact band you pick matters less than being consistent with the rest of your theme — Drupal core libraries hover around 0, and theme-level overrides typically land in the ±10 range. Reserve very negative values (e.g. < -50) for polyfills that genuinely must precede everything else.

Best practice: Prefer dependencies over weight for ordering. Dependencies are explicit and easier to reason about; reach for weight only when you need fine-grained ordering inside a single dependency cohort.

helix-component:
js:
dist/js/hx-button.js:
attributes:
type: module
async: true
crossorigin: anonymous
integrity: sha384-...

Common attributes:

  • type: module — ES module (required for HELiX)
  • defer: true — Defer execution until DOM ready
  • async: true — Load asynchronously (use with caution for modules)
  • crossorigin: anonymous — CORS mode for CDN assets
  • integrity: ... — Subresource Integrity (SRI) hash
helix-component:
js:
dist/js/hx-button.js:
preprocess: false # Do not aggregate
attributes:
type: module

When to set preprocess: false:

  • ES modules (Drupal aggregation doesn’t support ES modules)
  • Already-minified files
  • CDN assets
  • Files that break when concatenated

HELiX rule: Always set preprocess: false for web component files. ES modules cannot be safely aggregated.

helix-component:
js:
dist/js/hx-button.min.js:
minified: true
preprocess: false
attributes:
type: module

minified: true tells Drupal the file is already minified, so it skips minification during aggregation.

Tree-Shaking Pattern (Per-Component Libraries)

Section titled “Tree-Shaking Pattern (Per-Component Libraries)”

For optimal performance, define one library per component. This enables tree-shaking: only load components actually used on a page.

mytheme.libraries.yml
helix-all:
version: 3.9.0
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/core.js:
type: external
minified: true
preprocess: false
attributes:
type: module

Pros:

  • Simple single-library setup
  • One HTTP request
  • All components available globally

Cons:

  • Loads all components even if unused
  • Larger initial bundle size
  • No tree-shaking benefits
Section titled “Per-Component Approach (Optimized, Recommended)”
mytheme.libraries.yml
helix-button:
version: 3.9.0
js:
dist/js/hx-button.js:
preprocess: false
attributes:
type: module
helix-card:
version: 3.9.0
js:
dist/js/hx-card.js:
preprocess: false
attributes:
type: module
helix-text-input:
version: 3.9.0
js:
dist/js/hx-text-input.js:
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-form-base # Shared form utilities

Pros:

  • Load only components used on the page
  • Smaller initial bundles
  • Better caching (component updates don’t bust all caches)
  • Explicit dependencies between components

Cons:

  • More HTTP requests (mitigated by HTTP/2)
  • More complex library definitions
  • Requires build pipeline for per-component bundles
mytheme.libraries.yml
# Core utilities and shared dependencies
helix-core:
version: 3.9.0
js:
dist/js/helix-core.js:
preprocess: false
attributes:
type: module
# Common component bundle (buttons, badges, alerts)
helix-common:
version: 3.9.0
js:
dist/js/helix-common.js:
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
# Heavy components loaded on-demand
helix-data-table:
version: 3.9.0
js:
dist/js/hx-data-table.js:
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
helix-data-table:
version: 3.9.0
js:
dist/components/hx-data-table/index.js:
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core

(hx-chart is not a shipped HELiX component — substitute whichever heavy component your theme needs to load on demand, e.g. hx-data-table, hx-date-picker, or hx-combobox.)

Strategy:

  • helix-core — Shared base (design tokens, utilities)
  • helix-common — Frequently-used components (bundle together)
  • Individual libraries for heavy/specialized components

Load HELiX on every page via theme attachment:

mytheme.info.yml
name: My Theme
type: theme
core_version_requirement: ^10 || ^11
libraries:
- mytheme/helix-common # Loaded on every page

Use when:

  • Components are used site-wide (headers, footers, navigation)
  • Consistent page-to-page experience
  • Small bundle size (<50KB)

Load HELiX only on specific pages/content types:

mytheme.theme
/**
* Implements hook_preprocess_node().
*/
function mytheme_preprocess_node(&$variables) {
// Attach HELiX card library only for article nodes
if ($variables['node']->getType() === 'article') {
$variables['#attached']['library'][] = 'mytheme/helix-card';
}
}

Use when:

  • Components are page-specific (dashboards, forms)
  • Heavy components not needed globally
  • Optimizing for page speed

Attach libraries via Twig templates:

{# templates/components/card.html.twig #}
{{ attach_library('mytheme/helix-card') }}
<hx-card variant="{{ variant }}" elevation="{{ elevation }}">
<span slot="heading">{{ title }}</span>
{{ body }}
</hx-card>

Use when:

  • Using Single Directory Components (SDC)
  • Component-level encapsulation
  • Template-driven architecture

Attach libraries programmatically in render arrays:

// In a controller or custom block plugin
$build = [
'#type' => 'container',
'#attached' => [
'library' => [
'mytheme/helix-button',
'mytheme/helix-card',
],
],
'#markup' => '<hx-button variant="primary">Click Me</hx-button>',
];
return $build;

Use when:

  • Building render arrays in PHP
  • Custom blocks or controllers
  • Dynamic library attachment based on conditions

With HTTP/2 multiplexing, multiple small files are often faster than one large bundle:

# Optimized for HTTP/2 — many small libraries
helix-button:
js:
dist/js/hx-button.js: { preprocess: false, attributes: { type: module } }
helix-card:
js:
dist/js/hx-card.js: { preprocess: false, attributes: { type: module } }
helix-badge:
js:
dist/js/hx-badge.js: { preprocess: false, attributes: { type: module } }

Benefits:

  • Parallel downloads
  • Better caching granularity
  • True tree-shaking

Requirements:

  • HTTP/2 enabled on server
  • CDN with HTTP/2 support
  • Modern browser (all evergreens support HTTP/2)

Use resource hints for critical components:

helix-button:
js:
dist/js/hx-button.js:
preprocess: false
attributes:
type: module
header: true # Add to <head> instead of before </body>

Or via html.html.twig:

<link rel="modulepreload" href="/themes/custom/mytheme/dist/js/hx-button.js">

Drupal’s library system integrates with its cache system:

  1. Development — Disable caching (sites/default/services.yml)

    parameters:
    twig.config:
    debug: true
    auto_reload: true
    cache: false
  2. Production — Enable aggregation and caching

    • Admin → Performance → “Aggregate JavaScript files”
    • Set version in .libraries.yml to bust caches on updates
  3. CDN — Use versioned URLs for immutable caching

    helix-cdn:
    js:
    https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/core.js:
    type: external
    minified: true

Monitor per-component bundle sizes:

Terminal window
# In your theme build process
ls -lh dist/js/hx-*.js | awk '{print $5, $9}'

HELiX targets:

  • <5KB per component (minified + gzipped)
  • <50KB total bundle (all components)
mytheme.libraries.yml
helix-button-cdn:
version: 3.9.0
js:
https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button.js:
type: external
minified: true
preprocess: false
attributes:
type: module
crossorigin: anonymous

Usage in Twig:

{{ attach_library('mytheme/helix-button-cdn') }}
<hx-button variant="primary" hx-size="lg">
Submit Form
</hx-button>
mytheme.libraries.yml
helix-button:
version: VERSION # Uses theme version from .info.yml
js:
dist/js/hx-button.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- core/once

Theme build process (package.json):

{
"scripts": {
"build": "vite build",
"dev": "vite build --watch"
},
"dependencies": {
"@helixui/library": "^3.9.0"
}
}

Vite config (vite.config.js):

import { defineConfig } from 'vite';
export default defineConfig({
build: {
outDir: 'dist/js',
lib: {
entry: {
// The published @helixui/library package only ships the
// compiled `dist/` tree — there is no `src/` in the npm
// tarball, and the canonical per-component entry uses the
// package's `./components/<tag>` subpath export rather than
// a node_modules path.
'hx-button': 'node_modules/@helixui/library/dist/components/hx-button/index.js',
},
formats: ['es'],
},
rollupOptions: {
external: ['lit'],
},
},
});

If you’re consuming through the package’s exports map (recommended), prefer:

import '@helixui/library/components/hx-button';
Section titled “Example 3: hx-card Library (Full-Featured)”
mytheme.libraries.yml
helix-card:
version: 3.9.0
js:
dist/js/hx-card.js:
minified: true
preprocess: false
weight: 0
attributes:
type: module
dependencies:
- mytheme/helix-core
- core/once

With Drupal Behaviors:

mytheme.libraries.yml
helix-card:
version: 3.9.0
js:
dist/js/hx-card.js:
minified: true
preprocess: false
attributes:
type: module
js/helix-card-behavior.js: {} # Drupal behavior
dependencies:
- mytheme/helix-core
- core/drupal
- core/once

Behavior file (js/helix-card-behavior.js):

(function (Drupal, once) {
'use strict';
Drupal.behaviors.helixCard = {
attach(context) {
once('helix-card-init', 'hx-card[hx-href]', context).forEach((card) => {
card.addEventListener('hx-click', (e) => {
const { url } = e.detail;
// Optional: Integrate with Drupal's AJAX system
if (card.hasAttribute('data-use-ajax')) {
e.preventDefault();
// Trigger AJAX request
Drupal.ajax({ url }).execute();
} else {
// Normal navigation
window.location.href = url;
}
});
});
},
};
})(Drupal, once);

Twig template usage:

{# templates/content/node--article--teaser.html.twig #}
{{ attach_library('mytheme/helix-card') }}
<hx-card
variant="featured"
elevation="raised"
href="{{ url }}"
>
<img slot="image" src="{{ content.field_image }}" alt="{{ content.field_image.alt }}">
<span slot="heading">{{ label }}</span>
{{ content.body }}
<div slot="footer">
<time datetime="{{ node.createdtime }}">{{ node.createdtime|date('M j, Y') }}</time>
</div>
</hx-card>
mytheme.libraries.yml
# Core design tokens and utilities
helix-core:
version: 3.9.0
js:
dist/js/helix-core.js:
minified: true
preprocess: false
attributes:
type: module
# Common UI components (bundled for performance)
helix-common:
version: 3.9.0
js:
dist/js/helix-common.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
- core/once
# Individual form components
helix-text-input:
version: 3.9.0
js:
dist/js/hx-text-input.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
helix-select:
version: 3.9.0
js:
dist/js/hx-select.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
helix-checkbox:
version: 3.9.0
js:
dist/js/hx-checkbox.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core
# Form bundle (all form components)
helix-forms:
dependencies:
- mytheme/helix-text-input
- mytheme/helix-select
- mytheme/helix-checkbox
- mytheme/helix-radio-group
- mytheme/helix-textarea
- mytheme/helix-switch
# Heavy components (load on-demand)
helix-data-table:
version: 3.9.0
js:
dist/js/hx-data-table.js:
minified: true
preprocess: false
attributes:
type: module
dependencies:
- mytheme/helix-core

Conditional attachment in theme:

mytheme.theme
function mytheme_preprocess_page(&$variables) {
// Always load common components
$variables['#attached']['library'][] = 'mytheme/helix-common';
// Load form components on specific paths
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() === 'node.add' ||
$route_match->getRouteName() === 'node.edit') {
$variables['#attached']['library'][] = 'mytheme/helix-forms';
}
// Load data table on views pages
if ($route_match->getRouteName() === 'view.patients.page_1') {
$variables['#attached']['library'][] = 'mytheme/helix-data-table';
}
}

Symptom: <hx-button> renders as plain text, no styling

Check:

  1. Library attached? {{ attach_library('mytheme/helix-button') }}
  2. type: module attribute present in .libraries.yml?
  3. File path correct? Check browser DevTools Network tab
  4. Drupal cache cleared? drush cr

Symptom: Console error: Uncaught SyntaxError: Cannot use import statement outside a module

Fix: Add type: module attribute:

helix-button:
js:
dist/js/hx-button.js:
attributes:
type: module # Required!

Symptom: ReferenceError: Drupal is not defined

Fix: Add core/drupal dependency:

helix-behavior:
js:
js/helix-behavior.js: {}
dependencies:
- core/drupal # Ensures Drupal object is available

Symptom: Changes to .libraries.yml not reflected on site

Fix: Clear Drupal caches:

Terminal window
drush cr
# Or via UI: Admin → Configuration → Performance → "Clear all caches"

Symptom: Works in development, breaks in production with aggregation enabled

Fix: Set preprocess: false for ES modules:

helix-button:
js:
dist/js/hx-button.js:
preprocess: false # Do not aggregate
attributes:
type: module
  1. Always use type: module for HELiX component libraries
  2. Set preprocess: false for all ES modules (Drupal can’t aggregate them safely)
  3. Use semantic versioning — Increment version on updates to bust caches
  4. Prefer dependencies over weight — Explicit dependencies are clearer
  5. Tree-shake with per-component libraries — Load only what’s needed
  6. Use HTTP/2 — Multiple small files outperform large bundles
  7. Monitor bundle sizes — Keep components <5KB, total bundle <50KB
  8. Cache aggressively — Version your assets for long-term caching
  9. Test with aggregation enabled — Catch production issues early
  10. Document custom libraries — Comment complex dependency chains

Related Documentation: