Adding custom CSS and JS libraries in Drupal 10 theming
Drupal 10 treats every stylesheet and script as a managed asset, which gives site builders precise control over what loads on each page. The system replaces the old approach of dropping files into a theme directory and hoping for the best, replacing it with a structured library definition that the theme registry can aggregate, minify, and cache. For Australian teams working on government portals in Canberra or university sites across Melbourne, this control translates into predictable performance budgets and easier audits against the Australian Government Information Security Manual.
A custom library lives inside your theme's *.libraries.yml file and lists each CSS and JS file with its weight, group, and dependencies. Once defined, the library is attached through a preprocess hook, a twig template, or a render array. The rest of this walkthrough covers the mechanics of that workflow, the conditional loading patterns that come up on real builds, and the maintenance habits that keep a custom asset layer readable six months after launch.
Understanding asset libraries in Drupal 10
A library in Drupal 10 is simply a YAML file with a *.libraries.yml extension that describes a bundle of CSS, JS, settings, and dependencies. Each file within the bundle can declare a weight, a group, and a media query for stylesheets. The framework aggregates every file that shares the same group and media into a single request, which matters for projects hosted on Australian infrastructure where cross-continental latency to a US data centre can stretch a 200-millisecond budget into 800 milliseconds.
The other benefit of the library system is dependency resolution. If your custom slider script requires jQuery once and another agent jQuery UI Sortable, you list both under dependencies and Drupal ensures they appear in the correct order. The asset manager also takes care of versioning for cache busting, appending a query string tied to the file's modification time. This matters on sites behind the NBN where a stale cached asset can linger in browser caches for weeks after a deploy.
Defining your custom library in a theme
Start by creating a file named after your theme inside the theme directory, such as drupalwoo_theme.libraries.yml. Inside, define a key for your bundle and list the files. For example:
my-feature:
css:
theme:
css/my-feature.css: {}
js:
js/my-feature.js: {}
dependencies:
- core/jquery
The empty braces are deliberate: they signal that Drupal should apply the default weight and group. If you want your stylesheet to override a contributed module's CSS, set a heavier weight and a more specific group. For an Australian retail client in Brisbane rebuilding their product pages last spring, this approach let the team load custom carousel CSS after the Views module's defaults without touching the module directory.
After saving the file, clear the cache so the registry picks up the new library definition. The cache rebuild is required for the new file to register, so run drush cr or visit the performance settings page. From here, the library exists but is not yet attached to anything; the next step is wiring it into the rendering pipeline.
Attaching libraries to specific pages and templates
There are three common places to attach a library: a preprocess hook, a twig template, or a render array. Each method has trade-offs, and the choice often comes down to scope and team preference.
| Attachment method | Best for | Pros | Cons |
|---|---|---|---|
| Preprocess hook in .theme | Site-wide or route-specific assets | Runs before render, full PHP context, easy conditionals | Requires theme file edits, can be harder to trace |
attach_library() in twig |
Component-scoped assets | Stays with the template, obvious for front-end developers | Limited to template logic, runs during render |
#attached in render array |
Block, view, or controller output | Bundles with the content, travels with field formatters | Harder to apply across many pages |
Preprocess hooks offer the broadest control and are the right choice for site-wide assets. The pattern is:
function drupalwoo_theme_preprocess_html(&$variables) {
$variables['#attached']['library'][] = 'drupalwoo_theme/my-feature';
}
This attaches the bundle to every page render. For assets that only matter on certain routes, hook_preprocess_page or hook_preprocess_node is a better fit. A government client in Adelaide loading interactive maps only on contact pages used exactly this approach, attaching the mapping library through hook_preprocess_page when the route matched node/* and the node type was contact.
Attaching in twig is often the cleanest option when an asset belongs to a specific component. Inside a template, use {{ attach_library('drupalwoo_theme/my-feature') }} and the asset manager handles the rest. The third option, attaching via a render array in a custom block or controller, is useful when a library is bound to dynamic content rather than a static layout.
Loading libraries conditionally based on conditions
Conditional loading is where the library model genuinely shines. Common triggers include the current path, the user role, the response status code, and the presence of a specific field value. For a content-heavy publisher in Sydney, we attached a long-form reader script only on node types tagged with a dedicated vocabulary, which kept the homepage fast while still enabling the immersive layout on feature articles.
Roles are another useful trigger. Editors often need a different toolbar than anonymous visitors, and Drupal's role system can drive that. Use hook_preprocess_html or a custom service to inspect the user, then attach the relevant bundle. Time zone is also worth considering for sites serving audiences across Australia; a small JS helper to display local time can be attached only on event listing pages, leaving the rest of the site lean.
The condition logic should live in PHP rather than twig whenever possible. Twig conditionals execute during render, while preprocess hooks run earlier and avoid the cost of building markup that is later discarded. For agencies juggling multiple client sites across Sydney, Melbourne, and Perth, this discipline also keeps the asset layer consistent and easier to audit during security reviews.
Working with CDN resources in Australian projects
Sometimes the right answer is a third-party CDN rather than a local file. Common cases include font services, mapping libraries, and analytics scripts. Drupal handles external assets through the same library system by referencing a full URL in the file path:
external-fonts:
css:
theme:
https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap: { type: external }
The external type tells the asset manager not to try to read the file from disk, which avoids the file not found error that crops up when Drupal assumes a local path. For Australian government projects, this approach also aligns with the Digital Transformation Agency's preference for established CDNs over home-rolled asset hosts.
Be mindful of the Privacy Act when loading analytics from overseas CDNs, particularly for sites handling data that falls under the Notifiable Data Breaches scheme. A common pattern is to load analytics locally through a proxy module rather than directly from a US endpoint, which keeps visitor IPs inside the build's data residency boundary. Teams in Perth working on health sector projects have leaned on this proxy approach to satisfy the Office of the Australian Information Commissioner's expectations.
Debugging and performance considerations
When a library does not appear on the page, the first place to look is the cache. Drupal aggregates CSS and JS based on a hash of the attached libraries, and a stale entry will hide recent additions. Running drush cr or clearing through the status page resolves most surprises.
The second place to inspect is the source of the rendered page. Most browsers expose the aggregated CSS and JS file paths in their developer tools, and a quick scan shows whether your library merged with others. If your stylesheet is missing, walk back through the preprocess chain to confirm the attach code actually ran. A breakpoint in the .theme file or a temporary debug print will catch most typos in machine names.
Performance matters more in Australia than in markets with cheaper bandwidth. Connection speeds vary from fibre-to-the-premises in parts of Melbourne to satellite-only service in remote Western Australia, and a script-heavy page can punish the latter. Audit libraries with a tool like WebPageTest or the Chrome coverage panel, then trim anything not actively used. The asset system makes this easier than in older Drupal versions because unused libraries never load in the first place.
Best practices for maintenance and team workflows
Treat the libraries.yml file as code, with the same review standards as the rest of the theme. Name libraries by feature rather than by file, so a slider bundle is called slider and not owl-css. Group related assets under a single key to keep the dependency tree shallow, and split unrelated assets so a small change does not invalidate a larger cache.
Document the intent of each library in the theme's README. The next developer who joins the project, whether they are based in Hobart or joining remotely from another continent, should be able to read the file and understand why each bundle exists. If your team uses GitHub Actions or GitLab CI to run automated checks on every merge, include a script that fails the build if a library file is referenced but missing, which is a frequent slip.
Finally, review the full asset list once a quarter. Drupal module updates occasionally introduce new CSS or JS that interacts with your custom layers, and a periodic audit catches small drifts before they become tickets. For more on how this kind of workflow fits into the broader Drupal site administration practice covered on this site, the about page outlines the editorial approach and the topics that appear most often on the blog.