Two lessons in, you've built HTML with Twig and registered theme hooks with PHP — but everything you've seen so far has been completely unstyled. That lettered A/B/C/D list from the last two lessons? It's using upper-case letters as list markers because of exactly one CSS rule, shipped by the module, loaded through Drupal's library system. This lesson is about how that actually works.
Why libraries, and not just a global stylesheet?
In older PHP frameworks (and old Drupal, for that matter), it was common for every module to just dump its CSS and JS into the page on every single request, whether that page needed it or not. Drupal 8 and later took a deliberately different approach: CSS and JavaScript are declared as named, reusable libraries, and a library is only loaded on a page if something on that page explicitly asks for it. This keeps pages lean — a visitor never downloads CSS for a form they're not looking at — and it makes dependencies explicit and traceable instead of implicit and global.
What you'll learn in this lesson
- The structure of a
.libraries.ymlfile and how a library gets its full machine name - The five CSS grouping categories Drupal uses to control cascade order
- What the
preprocessoption does, and why you'd turn it off during development - How a render array actually pulls a library onto the page via
#attached - Two other ways to attach a library — from a Twig template, and via dependencies between libraries
The source file
Path: modules/theming_example/theming_example.libraries.yml
list:
css:
theme:
css/theming_example.css: {preprocess: false}
Four lines. This is genuinely the entire library definition responsible for that lettered list you saw in the last two lessons.
How it works, piece by piece
One file, one or more libraries
A .libraries.yml file sits at the root of a module directory, named MODULE_NAME.libraries.yml — here, theming_example.libraries.yml. Every top-level key inside it defines one library. This file defines exactly one, named list. Whenever you need to refer to this library elsewhere — in PHP or in Twig — you use its full machine name, which is always MODULE_NAME/LIBRARY_NAME: in this case, theming_example/list.
The css key and cascade groups
Under css, files are organized into named groups that control load order and specificity in the final stylesheet cascade:
base— CSS resets and normalize sheets, loaded firstlayout— major structural layout rulescomponent— standalone, reusable UI componentsstate— dynamic state rules, like hidden/active togglestheme— visual/skin styles: colors, fonts, decorative rules
This library places its file under theme — the right choice, since setting a list-style marker is purely decorative, not structural.
The file path and the preprocess option
css/theming_example.css: {preprocess: false}
The key itself, css/theming_example.css, is a path relative to the module's own root folder — not an absolute URL. The value next to it is a small options object; the only option set here is preprocess: false.
preprocess controls whether Drupal's CSS aggregation system is allowed to bundle this file together with other CSS into one combined, minified file for production performance. Setting it to false keeps this specific file separate — useful while actively developing (so you can see exactly which file changed without clearing an aggregate cache) or for assets that must never be altered by the bundling process. In a real production module you'd typically leave this option out entirely, since the default is true and lets Drupal optimize the file automatically.
Attaching the library to a render array with #attached
Declaring a library does not load it anywhere by itself — it's just a definition sitting on the shelf until something asks for it. The controller behind the page you've been visiting, ThemingPageController::list(), attaches it explicitly:
$build['render_version'] = [
'#theme' => 'item_list',
'#attached' => ['library' => ['theming_example/list']],
'#title' => $title,
'#items' => $items,
];
#attached is a special render array property. Its library key takes an array of one or more full library machine names. When Drupal renders this render array, it resolves every attached library and injects the corresponding <link> (for CSS) or <script> (for JS, unless configured to load in the footer) tags into the final page — you never write those tags by hand.
Worth noting: the same page attaches this library a second time, on a second render array further down in the same controller method. That's not a bug or an inefficiency — Drupal automatically deduplicates library attachments across an entire page render, so the CSS file is included exactly once in the final HTML no matter how many separate render arrays ask for it.
The CSS itself
/*
* style the list
* for OL you can have
* decimal | lower-roman | upper-roman | lower-alpha | upper-alpha
* for UL you can have
* disc | circle | square or an image eg url(x.png)
* you can also have 'none'
*/
.theming-example-list {
list-style-type: upper-alpha;
}
One rule, targeting the theming-example-list class — the exact class applied by the theming-example-list.html.twig template you saw in the earlier two lessons. This is the full pipeline made concrete, start to finish: the library declares a CSS file → a render array attaches that library → the Twig template outputs an element with the matching class → the browser applies the rule. That's why the list you've been looking at renders with letters (A, B, C, D) instead of Drupal's default bullet points.
JavaScript follows the exact same pattern
This particular library happens to contain only CSS, but Drupal libraries support JavaScript identically. A more complete definition with both would look like:
my_feature:
css:
theme:
css/my_feature.css: {}
js:
js/my_feature.js: {}
dependencies:
- core/jquery
- core/drupal
The js key maps file paths to their own options hash, using the same shape as css. The dependencies key lists other library machine names that must be loaded first — this is how you guarantee, say, that jQuery is available before your own script runs, without relying on jQuery just happening to already be loaded globally somewhere else on the page. This particular theming_example/list library has no dependencies at all, since a single self-contained CSS rule needs nothing else to function.
Attaching a library directly from Twig
There's a second way to attach a library that doesn't go through PHP's #attached property at all — directly inside a template:
{{ attach_library('theming_example/list') }}
This is handy when a template should always pull in a particular library no matter how or where it gets rendered — the attachment travels with the template file itself rather than depending on every caller remembering to add it to their render array.
See it for yourself
You've technically already seen this library's effect twice — now look again knowing exactly why it's there.
The lettered list (A, B, C, D) is the direct, visible result of the CSS rule examined above being loaded via #attached and applied to an element carrying the theming-example-list class. If you disabled this library — or if the CSS class name in the Twig template didn't match the CSS selector exactly — this list would fall back to plain, unstyled bullet points.
Quick check: you attach the same library on three different render arrays on the same page. Does the browser load the CSS file three times, once, or does it depend on the theme? If you said "once," you're right — Drupal deduplicates library attachments automatically across the whole page.
Key takeaways
- A module declares libraries in
MODULE_NAME.libraries.yml; each top-level key is a library, and its full machine name elsewhere is alwaysMODULE_NAME/LIBRARY_NAME. - CSS files are organized into cascade groups (
base,layout,component,state,theme) — choosethemefor purely decorative or skin-level styles. - The
preprocess: falseoption keeps a CSS file out of Drupal's aggregation bundle; omit it in production to let Drupal aggregate and minify automatically. - Libraries are never loaded globally by default — attach them precisely where needed with
#attached => ['library' => ['MODULE_NAME/LIBRARY_NAME']]on a render array, or with{{ attach_library('MODULE_NAME/LIBRARY_NAME') }}directly in a Twig template. - Drupal deduplicates library attachments automatically across a page render, so attaching the same library from multiple places never produces duplicate
<link>or<script>tags. - JavaScript assets follow the identical pattern under a
jskey, and adependencieslist ensures prerequisite libraries (likecore/jqueryorcore/drupal) load first, in order.
Coming up next
CSS gives your module's output a look. JavaScript gives it behavior — and Drupal has just as strong an opinion about how you should write that JavaScript as it does about CSS and Twig. In the next topic, we'll look at Drupal.behaviors, the pattern Drupal uses to make sure your JavaScript runs correctly on both the initial page load and every subsequent AJAX update, without ever running twice on the same element.