Skip to content

TutorialFor theme developersDrupal 11

Build your first theme 3: libraries, CSS, and breakpoints

In part 3 of the theme tutorial the theme gets its own CSS. You define an asset library, understand how its categories control load order, and declare the breakpoints that part 6 turns into responsive images.

Three files that give First theme its visual foundation:

  • css/base.css, the global stylesheet,
  • first_theme.libraries.yml, the asset library that delivers it (and, in part 5, JavaScript),
  • first_theme.breakpoints.yml, machine-readable descriptions of your design’s media queries.

The first_theme/global library that first_theme.info.yml has referenced since part 1 finally exists at the end of this part.

  • First theme as part 2 left it: installed, default, with your node.html.twig and node--teaser.html.twig templates.

Create css/base.css in the theme (replacing your generated starterkit css/ directory contents is fine; nothing attaches them anymore since you trimmed libraries: in part 1):

web/themes/custom/first_theme/css/base.css
/**
* @file
* Base styles for the First theme, loaded globally via first_theme/global.
*/
:root {
--first-theme-max-width: 70rem;
--first-theme-text: #1f2933;
--first-theme-accent: #0678be;
}
body {
margin: 0;
color: var(--first-theme-text);
font-family: system-ui, sans-serif;
line-height: 1.6;
}
a {
color: var(--first-theme-accent);
}
.layout-container {
max-width: var(--first-theme-max-width);
margin-inline: auto;
padding-inline: 1rem;
}
/* Tablet and up: see first_theme.breakpoints.yml for the shared values. */
@media (min-width: 48rem) {
.layout-container {
padding-inline: 2rem;
}
}

Plain CSS, nothing Drupal-specific: custom properties on :root, element defaults, and a .layout-container width constraint that matches a class the page template already prints. The media query value (48rem) reappears in step 4; keeping the two in sync is your job, because CSS cannot read YAML.

Also create an empty js/first-theme.js now (for example with touch web/themes/custom/first_theme/js/first-theme.js). The library you define next references it, and part 5 fills it in.

Drupal never loads loose CSS or JS files; it loads asset libraries, named bundles of files plus metadata, so it can deduplicate them across modules and themes, order them, aggregate them, and attach them only where needed. Replace the contents of first_theme.libraries.yml with:

web/themes/custom/first_theme/first_theme.libraries.yml
# An asset library bundles CSS and JS so Drupal can attach, aggregate and
# cache them. "global" is attached site-wide via first_theme.info.yml.
global:
css:
# The "base" / "layout" / "component" / "theme" categories control
# aggregation order (SMACSS).
base:
css/base.css: {}
js:
js/first-theme.js: {}
dependencies:
- core/drupal
- core/once

Each key under the library name carries weight:

  • css: is split into SMACSS categories (Scalable and Modular Architecture for CSS): base, layout, component, state, and theme. The parser, LibraryDiscoveryParser, maps each category to a numeric weight (base is -200, theme is +200), and that weight decides the order rules end up in, across every library on the page. Element defaults load first, overrides load last, regardless of which module or theme contributed them.
  • The empty braces after each file accept per-file options such as minified: true or media: print. Empty is the common case, but the key must be there.
  • js: lists JavaScript files the same way.
  • dependencies: guarantees other libraries load first. core/drupal provides the Drupal JavaScript object and the behavior system; core/once provides the once() function. Part 5 uses both; declaring dependencies you do not consume yet costs two small, cached files.

Library definitions are cached, so:

Terminal window
ddev drush cache:rebuild

No edit to first_theme.info.yml is needed: its libraries: list has named first_theme/global since part 1, and now the reference resolves. Attaching from the info file is the right tool for site-wide assets; for assets that belong to one template or one condition, themes attach libraries from preprocess functions or hook_page_attachments_alter() instead. Asset libraries covers those patterns.

Your CSS already responds to viewport widths, but PHP cannot see media queries inside a .css file. Breakpoints exist to state them in a form the server can use, most importantly so the Responsive Image module can generate a different image size per screen width. Create first_theme.breakpoints.yml:

web/themes/custom/first_theme/first_theme.breakpoints.yml
# Breakpoints let Drupal's responsive image styles and other modules react
# to the same screen sizes your CSS uses. Mobile first: smallest has no
# media query.
first_theme.mobile:
label: Mobile
mediaQuery: ''
weight: 0
multipliers:
- 1x
- 2x
first_theme.tablet:
label: Tablet
mediaQuery: 'all and (min-width: 48rem)'
weight: 1
multipliers:
- 1x
- 2x
first_theme.desktop:
label: Desktop
mediaQuery: 'all and (min-width: 75rem)'
weight: 2
multipliers:
- 1x
- 2x

The file format comes from the Breakpoint module’s plugin manager (the breakpoint.manager service):

  • Each top-level key is a breakpoint ID, conventionally prefixed with the theme name.
  • mediaQuery is the literal media query string. The mobile entry’s is empty: mobile-first means the smallest width is the unqualified default, not a query.
  • weight orders breakpoints from smallest to largest.
  • multipliers lists the pixel-density variants (1x, 2x) you intend to serve; high-density screens get a double-resolution image at the same layout size.
  • All of a theme’s breakpoints form a breakpoint group named after the theme, which is what you will select in part 6.

Declaring breakpoints does nothing visible by itself. There is no UI for them and no effect on your CSS; they are data, consumed by whatever asks the breakpoint manager. The Breakpoint module is installed by the standard profile; on a minimal site, ddev drush pm:install breakpoint first.

Library definitions are cached: rebuild caches after creating or editing *.libraries.yml. Then check the page source. If the library is attached but the file 404s, the path in the YAML is wrong; paths are relative to the theme directory.

Drupal aggregates CSS and JS by default, and your browser caches the aggregates. While developing, turn aggregation off under Administer → Configuration → Development → Performance/admin/config/development/performance by unchecking Aggregate CSS files and Aggregate JavaScript files.

My styles load before the ones they should override

Section titled “My styles load before the ones they should override”

Check the category. A rule in base loads before everything; if you are overriding another library’s component styles, the override belongs in theme (or at least the same category). Categories beat file order.

“The first_theme/global library defines a positive weight”

Section titled ““The first_theme/global library defines a positive weight””

Library JS files reject positive weight values by throwing this exception; only small negative weights are allowed, and dependencies are the supported way to order JavaScript. Remove the weight and declare a dependency instead.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.