CDN Installation
apps/docs/src/content/docs/drupal/installation/cdn Click to copy apps/docs/src/content/docs/drupal/installation/cdn CDN installation is the fastest way to add HELiX web components to a Drupal site. It requires no build pipeline, no npm, and no compiled assets — just a library definition in your theme’s .libraries.yml file pointing to the jsDelivr or unpkg CDN.
This guide covers the complete CDN workflow: basic setup, version pinning strategies, per-component loading, SRI hash verification, CSS bundle loading, fallback patterns, and cache management.
When to Use CDN Installation
Section titled “When to Use CDN Installation”Well suited for:
- Rapid prototyping and proof-of-concept work
- Small-to-medium sites without existing JavaScript build pipelines
- Third-party or contrib themes you don’t maintain
- Teams without Node.js or front-end build expertise
- Short-lived projects where setup overhead is not justified
Not suited for:
- HIPAA-compliant or patient-facing applications (external dependency risk)
- Sites behind restrictive corporate firewalls that block external CDNs
- Applications with strict Content Security Policy rules blocking external scripts
- Environments requiring offline development
- High-traffic sites where CDN downtime would be unacceptable
How CDN Loading Works in Drupal
Section titled “How CDN Loading Works in Drupal”Drupal’s Libraries API manages JavaScript and CSS assets. When you define a library with a URL as the asset path and type: external, Drupal renders a <script> tag pointing to that URL. Adding attributes: { type: module } marks it as an ES module, which is required for HELiX components built with Lit.
Loading sequence:
- Drupal renders a
<script type="module" src="https://cdn.jsdelivr.net/...">tag - Browser downloads the ES module from the CDN
- Module registers all HELiX custom elements (
hx-button,hx-card, etc.) - Twig templates render those elements as standard HTML tags
- Browser upgrades each custom element with its Shadow DOM and styles
Step 1: Define the Library
Section titled “Step 1: Define the Library”Create or edit your theme’s .libraries.yml file.
Strategy B — core + per-component (recommended, jsDelivr)
Section titled “Strategy B — core + per-component (recommended, jsDelivr)”helix-components: version: 3.0.0 js: # Core: registry + tokens, ~8.4KB min+gz https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/cdn/core.js: type: external attributes: type: module preprocess: false # Per-component modules — ~2KB each; only list what the theme uses https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/cdn/hx-button.js: type: external attributes: type: module preprocess: false https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/cdn/hx-card.js: type: external attributes: type: module preprocess: falseField explanations:
helix-components— Library machine name. Referenced asmytheme/helix-componentsin templates and hooks.version: 3.0.0— Drupal’s internal library version for cache invalidation. Increment this when you update the CDN URL.type: external— Tells Drupal this is a URL, not a local file path.attributes: { type: module }— Required. Without this, browsers load the script as a classic script and ES moduleimportstatements fail.preprocess: false— Prevents Drupal from attempting to aggregate this external URL.
Strategy A — single-file bundle (prototyping, not recommended for production)
Section titled “Strategy A — single-file bundle (prototyping, not recommended for production)”helix-components: version: 3.0.0 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: falseThe @helixui/library package.json declares main: "./dist/index.js" — this is the entry point for bundler-driven builds. Loading it from a CDN pulls every component (~150KB gzipped) whether the site uses it or not.
unpkg mirror
Section titled “unpkg mirror”Both jsDelivr and unpkg are reliable and serve correct ES module MIME types. jsDelivr is recommended for production due to its multi-CDN architecture (Cloudflare + Fastly + CloudFront) and higher uptime SLA.
# Swap the host — same paths work on unpkghelix-components: version: 3.0.0 js: https://unpkg.com/@helixui/library@3.9.0/dist/cdn/core.js: type: external attributes: type: module preprocess: falseCSS Bundle
Section titled “CSS Bundle”To load the HELiX CSS bundle alongside the JavaScript:
helix-components: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: false css: theme: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/css/helix-all.css: type: external preprocess: falseThe helix-all.css bundle includes design token custom properties and base styles used by all components. Load it if your theme is not already importing the CSS through a separate channel (such as npm-installed tokens).
Step 2: Attach the Library
Section titled “Step 2: Attach the Library”Global Attachment (Every Page)
Section titled “Global Attachment (Every Page)”Add the library to your theme’s .info.yml:
name: My Healthcare Themetype: themecore_version_requirement: ^10 || ^11base theme: stable9
libraries: - mytheme/helix-componentsThis loads HELiX on every page of your site. Suitable when components appear across most page types.
Template-Level Attachment
Section titled “Template-Level Attachment”For performance-sensitive sites, attach the library only where components are used:
{# templates/node--article.html.twig #}
{{ attach_library('mytheme/helix-components') }}
<article{{ attributes }}> <hx-card variant="featured"> <span slot="heading">{{ node.title.value }}</span> {{ content.body }} </hx-card></article>Hook-Based Attachment
Section titled “Hook-Based Attachment”Attach programmatically based on route, content type, or other conditions:
/** * Implements hook_page_attachments(). */function mytheme_page_attachments(array &$attachments) { // Attach HELiX on all pages. $attachments['#attached']['library'][] = 'mytheme/helix-components';}Or conditionally:
/** * Implements hook_page_attachments(). */function mytheme_page_attachments(array &$attachments) { $route = \Drupal::routeMatch()->getRouteName();
// Only load on node pages. if (str_starts_with($route, 'entity.node')) { $attachments['#attached']['library'][] = 'mytheme/helix-components'; }}Step 3: Use Components in Templates
Section titled “Step 3: Use Components in Templates”Once the library is attached, HELiX custom elements are available in any Twig template on the same page.
Button
Section titled “Button”<hx-button variant="primary" hx-size="lg">Save Changes</hx-button><hx-button variant="secondary" hx-size="md">Cancel</hx-button><hx-button variant="ghost" hx-size="sm">Learn More</hx-button>Note: Use hx-size (not size) — HELiX uses the hx- prefix on size and other attributes to avoid conflicts with native HTML element attributes.
<hx-card variant="elevated"> <span slot="heading">{{ node.title.value }}</span> <p>{{ content.body }}</p></hx-card>For link cards, use href (not href):
<hx-card variant="outlined" href="{{ url('entity.node.canonical', {'node': node.id}) }}"> <span slot="heading">{{ node.title.value }}</span> <p>{{ node.field_summary.value }}</p></hx-card>Version Pinning
Section titled “Version Pinning”Exact Version (Production Recommended)
Section titled “Exact Version (Production Recommended)”helix-components: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: falseExact version pinning guarantees identical behavior across all environments and deployments. The CDN serves versioned URLs with Cache-Control: public, max-age=31536000, immutable — meaning the browser caches the file permanently and never refetches it for the same URL.
Updating to a New Version
Section titled “Updating to a New Version”When a new HELiX version is available:
- Update the CDN URL version number
- Update the Drupal library
versionfield to match (triggers Drupal cache invalidation) - Clear Drupal caches:
drush cr - Hard-reload the browser (Cmd+Shift+R) to bypass browser cache
# Beforehelix-components: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: false
# After upgradehelix-components: version: 1.2.0 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: falseNever Use @latest in Production
Section titled “Never Use @latest in Production”# DO NOT do this in productionjs: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module@latest resolves to whatever version is current at request time. A breaking HELiX release would instantly propagate to your production site with no review or testing. Reserve @latest for local sandbox exploration only.
Per-Component Loading
Section titled “Per-Component Loading”Loading the full bundle delivers all HELiX components. If your site uses only a few components, load them individually to reduce the browser’s initial download.
Per-Component Library Definitions
Section titled “Per-Component Library Definitions”helix-button: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-button/index.js: type: external attributes: type: module preprocess: false
helix-card: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-card/index.js: type: external attributes: type: module preprocess: false
helix-text-input: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/components/hx-text-input/index.js: type: external attributes: type: module preprocess: falseThe per-component CDN path pattern is:
https://cdn.jsdelivr.net/npm/@helixui/library@{version}/dist/components/{component-name}/index.jsAttaching Per-Component Libraries in Templates
Section titled “Attaching Per-Component Libraries in Templates”{# Article template — only needs card #}{{ attach_library('mytheme/helix-card') }}
<hx-card variant="elevated"> <span slot="heading">{{ node.title.value }}</span> {{ content.body }}</hx-card>{# Contact form template — only needs button and text-input #}{{ attach_library('mytheme/helix-button') }}{{ attach_library('mytheme/helix-text-input') }}
<form> <hx-text-input name="email" label="Email Address" required></hx-text-input> <hx-button variant="primary" hx-size="md" type="submit">Submit</hx-button></form>Trade-off: Per-component loading reduces payload per page but increases the number of distinct CDN requests. With HTTP/2 multiplexing, the overhead per request is minimal, so this trade-off generally favors per-component loading when a page uses three or fewer components.
SRI Hash Verification
Section titled “SRI Hash Verification”Subresource Integrity (SRI) lets browsers verify that CDN-delivered files haven’t been tampered with. When the browser fetches the script, it computes a hash and compares it to the integrity attribute — if they don’t match, the script is blocked.
Generating the SRI Hash
Section titled “Generating the SRI Hash”curl -s https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js | \ openssl dgst -sha384 -binary | \ openssl base64 -AThis outputs a base64-encoded SHA-384 hash. Prepend sha384- to form the integrity value.
Adding SRI to the Library Definition
Section titled “Adding SRI to the Library Definition”helix-components: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module integrity: sha384-REPLACE_WITH_ACTUAL_HASH crossorigin: anonymous preprocess: falseThe crossorigin: anonymous attribute is required when using integrity — without it the browser cannot perform the integrity check on cross-origin resources.
Operational note: You must regenerate the hash every time you update to a new HELiX version. Automate this in your deployment pipeline or store the hash alongside your libraries.yml in source control.
Content Security Policy
Section titled “Content Security Policy”If your site uses a Content Security Policy, whitelist the CDN domains to permit the external script load.
In settings.php (Security Kit module)
Section titled “In settings.php (Security Kit module)”$config['security_kit.settings']['seckit_xss']['csp']['script-src'] = [ "'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com',];
$config['security_kit.settings']['seckit_xss']['csp']['style-src'] = [ "'self'", 'https://cdn.jsdelivr.net',];In .htaccess (Apache)
Section titled “In .htaccess (Apache)”Header always set Content-Security-Policy "script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net;"CDN Fallback Pattern
Section titled “CDN Fallback Pattern”CDNs can experience downtime. For production sites where component availability matters, implement a local fallback.
Library Structure with Fallback
Section titled “Library Structure with Fallback”web/themes/custom/mytheme/├── mytheme.libraries.yml├── js/│ └── helix-cdn-fallback.js # Fallback detection script└── libraries/ └── helix/ └── dist/ └── index.js # Local copy downloaded from npmLibraries Definition
Section titled “Libraries Definition”helix-components: version: 1.1.2 js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: false js/helix-cdn-fallback.js: attributes: type: module preprocess: falseFallback Script
Section titled “Fallback Script”// Wait for the CDN module to register custom elements.// hx-button is the smallest component — if it loaded, the bundle loaded.setTimeout(() => { if (!customElements.get('hx-button')) { console.warn('[HELiX] CDN load failed — switching to local fallback'); import('/themes/custom/mytheme/libraries/helix/dist/index.js') .then(() => console.info('[HELiX] Local fallback loaded successfully')) .catch((err) => console.error('[HELiX] Local fallback also failed:', err)); }}, 2000);Download the local copy to include in your theme:
cd web/themes/custom/mytheme/libraries/helixnpm pack @helixui/library@3.9.0tar -xf helixui-library-3.9.0.tgz --strip-components=1 package/distrm helixui-library-3.9.0.tgzPerformance Optimization
Section titled “Performance Optimization”Preconnect to CDN
Section titled “Preconnect to CDN”Add a preconnect hint to your html.html.twig override. This tells the browser to establish a TCP/TLS connection to the CDN origin before it encounters the <script> tag, saving 100–300ms on first page load:
{# templates/html.html.twig #}
<head> <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin> <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
{{ page_top }} <head-placeholder token="{{ placeholder_token }}"> <title>{{ head_title|safe_join(' | ') }}</title> <css-placeholder token="{{ placeholder_token }}"> <js-placeholder token="{{ placeholder_token }}"></head>Load in Head
Section titled “Load in Head”By default Drupal places scripts at the bottom of <body>. To load HELiX earlier (reduces flash of unstyled custom elements):
helix-components: version: 1.1.2 header: true js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module preprocess: falseVerifying Installation
Section titled “Verifying Installation”Browser DevTools Checklist
Section titled “Browser DevTools Checklist”- Network tab — Filter by
index.js. The CDN request should show status200 OKand typescript. - Console — No errors from
@helixui/library. - Elements tab — Inspect an
<hx-button>element. It should have#shadow-root (open)inside it.
Console Verification
Section titled “Console Verification”// Returns the constructor function if HELiX loaded successfullycustomElements.get('hx-button');
// Lists all HELiX custom elements present on the page[...document.querySelectorAll('*')] .filter(el => el.tagName.startsWith('HX-')) .map(el => el.tagName.toLowerCase());Troubleshooting
Section titled “Troubleshooting”Components render as unstyled text
Section titled “Components render as unstyled text”Cause: The attributes: { type: module } line is missing or indented incorrectly in .libraries.yml.
Fix:
helix-components: js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module # This line must be present and indented under attributes preprocess: falseCDN request returns 404
Section titled “CDN request returns 404”Cause: Wrong file path in the URL.
Fix: The 3.0.0 CDN layout ships dist/cdn/core.js (registry + tokens) plus per-component modules at dist/cdn/hx-<component>.js. Legacy single-file paths from pre-3.0 releases are not published and will return 404. The npm entry for bundlers is dist/index.js.
Components load but appear unstyled (no color/spacing)
Section titled “Components load but appear unstyled (no color/spacing)”Cause: CSS bundle not loaded, or design tokens missing.
Fix: Add the CSS bundle to your library definition:
css: theme: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/css/helix-all.css: type: external preprocess: falseStale components after version update
Section titled “Stale components after version update”Cause: Drupal has cached the old library definition, or the browser has cached the old script URL.
Fix:
drush crThen hard-reload in the browser (Cmd+Shift+R on Mac, Ctrl+Shift+R on Windows/Linux).
Ensure the Drupal library version field also changed — Drupal uses it to generate cache-busting query strings.
CORS error in console
Section titled “CORS error in console”Cause: Missing crossorigin attribute (required when using integrity).
Fix:
attributes: type: module crossorigin: anonymousComplete Production-Ready Example
Section titled “Complete Production-Ready Example”mytheme.info.yml:
name: My Healthcare Themetype: themecore_version_requirement: ^10 || ^11base theme: stable9
libraries: - mytheme/helix-componentsmytheme.libraries.yml:
helix-components: version: 1.1.2 header: true js: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/index.js: type: external attributes: type: module crossorigin: anonymous preprocess: false css: theme: https://cdn.jsdelivr.net/npm/@helixui/library@3.9.0/dist/css/helix-all.css: type: external preprocess: falsetemplates/node--article.html.twig:
<article{{ attributes }}> <hx-card variant="elevated" href="{{ url('entity.node.canonical', {'node': node.id}) }}"> <span slot="heading">{{ node.title.value }}</span> {{ content.body }} <div slot="footer"> <hx-button variant="secondary" hx-size="sm">Read More</hx-button> </div> </hx-card></article>Production Checklist
Section titled “Production Checklist”- Exact version pinned (
@1.1.2, not@latest) -
attributes: { type: module }present in library definition -
preprocess: falseset to prevent aggregation errors - Drupal library
versionfield matches HELiX version - Caches cleared after library changes (
drush cr) - CDN request verified in browser Network tab (HTTP 200)
- Shadow DOM visible in Elements tab for at least one component
-
crossorigin: anonymousadded if usingintegritySRI hash - CSP headers updated to whitelist
cdn.jsdelivr.netif applicable - Preconnect hint added to
html.html.twigfor performance
Next Steps
Section titled “Next Steps”- npm Installation — Move to a local build pipeline for tree-shaking and offline development
- Module Installation — Package HELiX as a Drupal module for multi-site deployments