Skip to content

Loading Scripts & CSS

import { Aside } from ‘@astrojs/starlight/components’;

ResourceLoader.js loads JavaScript via <script> elements and CSS via <link rel="stylesheet"> elements injected into the DOM. This guide covers all the behavioral details you need for production use.

ResourceLoader.include(['https://cdn.example.com/library.js'])
.then((results) => {
console.log('Script loaded:', results[0].value); // the URL
// Library is now available globally — use it here
})
.catch((error) => {
console.error('Load failed:', error);
});
ResourceLoader.include(['https://cdn.example.com/styles.css'])
.then(() => {
console.log('Styles applied!');
});

You can mix JS, CSS, and other types in a single include() call. All resources load concurrently (up to maxConcurrency) and the Promise resolves when every resource has completed:

ResourceLoader.include([
'https://cdn.example.com/normalize.css',
'https://cdn.example.com/app.css',
'https://cdn.example.com/vendor.js',
'https://cdn.example.com/app.js',
])
.then((results) => {
// results is an array — one entry per URL
// [
// { status: 'fulfilled', value: 'https://cdn.example.com/normalize.css' },
// { status: 'fulfilled', value: 'https://cdn.example.com/app.css' },
// { status: 'fulfilled', value: 'https://cdn.example.com/vendor.js' },
// { status: 'fulfilled', value: 'https://cdn.example.com/app.js' },
// ]
console.log(`Loaded ${results.length} resources`);
initApp();
});

By default, scripts are inserted into <head>. Set appendToBody: true to insert them at the end of <body> instead:

ResourceLoader.include(['https://cdn.example.com/app.js'], {
appendToBody: true,
});

Use appendToBody: true when your script references DOM elements that must exist before the script runs, and you are loading the script before the page finishes rendering.

By default (deferScriptsUntilReady: true), scripts wait for the DOMContentLoaded event before being inserted. This prevents scripts from executing before the DOM is ready.

If the DOM is already ready when include() is called (which is almost always the case in practice), this option has no effect. Set it to false only if you need scripts to be inserted immediately without any DOMContentLoaded check:

ResourceLoader.include(['https://cdn.example.com/app.js'], {
deferScriptsUntilReady: false,
});

ResourceLoader.js checks whether a resource with the same URL is already present in the DOM before inserting a new element. If a <script src="..."> or <link href="..."> element matching the URL already exists, the load is skipped and the Promise resolves immediately.

This means calling include() multiple times with the same URL is safe — the script or stylesheet will only ever be loaded once per page:

// Call include() twice with the same URL — no duplicate loads
await ResourceLoader.include(['https://cdn.example.com/lib.js']);
await ResourceLoader.include(['https://cdn.example.com/lib.js']); // no-op, already in DOM

When loading scripts or stylesheets from a third-party CDN, you should use Subresource Integrity hashes to verify that the file has not been tampered with.

Pass the integrity hash via the attributes option:

ResourceLoader.include([
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
], {
crossorigin: 'anonymous',
attributes: {
integrity: 'sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD',
},
});

The crossorigin attribute is required when using SRI for cross-origin resources. Valid values are:

ValueBehavior
false (default)No crossorigin attribute set
'anonymous'Sends requests without cookies or credentials
'use-credentials'Sends cookies and credentials with requests

For CDN resources with SRI, use 'anonymous' unless the CDN requires credentials.


When a <script> or <link> element fails to load (e.g., the file is not found), ResourceLoader.js removes the element from the DOM by default (removeFailedElements: true). This prevents half-loaded, broken element references from persisting on the page.

Set to false if you want the failed element to remain in the DOM for debugging:

ResourceLoader.include(['https://cdn.example.com/missing.css'], {
removeFailedElements: false, // Leave the failed <link> element in the DOM
onError: (error, url) => {
console.error(`Failed: ${url}`, error);
},
});

All examples work equally well with async/await:

async function loadDependencies() {
try {
await ResourceLoader.include([
'https://cdn.example.com/styles.css',
'https://cdn.example.com/vendor.js',
]);
console.log('Dependencies ready');
initApp();
} catch (error) {
if (error.type === 'aggregate') {
error.results
.filter(r => r.status === 'rejected')
.forEach(r => console.error(`Failed: ${r.url}`, r.reason));
}
}
}
loadDependencies();

Full Example: Conditionally Load a Library

Section titled “Full Example: Conditionally Load a Library”

A common pattern is to load a library only if it hasn’t been loaded yet:

async function ensureChartsLoaded() {
const chartUrl = 'https://cdn.example.com/chart.js';
if (ResourceLoader.getResourceState(chartUrl) === 'loaded') {
return; // Already loaded, nothing to do
}
await ResourceLoader.include([chartUrl], {
retries: 2,
timeout: 10000,
});
// Chart.js is now available globally
renderChart();
}