Accessibility and Performance in Themes

This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production.

This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages.

A theme controls most of the markup, styling and scripting a visitor meets, so it decides much of whether a Drupal site can be used with a keyboard, a screen reader or zoomed text, and much of how quickly the first screen appears. This page covers both halves for the Meridian Museum theme meridian: what core promises about accessibility, the markup and JavaScript APIs a theme uses to keep that promise (skip links, focus styles, Drupal.announce and the tabbing manager), the editor-side checker that ships with Drupal CMS, and then the performance work a theme owns (Core Web Vitals, font loading, image formats, lazy loading, critical CSS and Lighthouse).

This page was written against Drupal 11.4.x, Drush 13/14 and PHP 8.3 or later, and checked on 2026-10-30. Drupal 10 ends on 2026-12-09, so new themes should target Drupal 11. Drupal 12.0.0 is planned for the week of 2026-12-07; it removes Olivero, Claro and Stable 9 from core, which matters for anything below that copies markup from those themes.

General accessibility principles, WCAG conformance levels and legal requirements are explained in Web Accessibility; the browser-side performance fundamentals are in Performance: Loading and Native HTML Images and Lazy Loading. This page applies them to Drupal themes. The theme files themselves are introduced in Theme Info and Regions and Libraries and Assets.

What core commits to

Drupal core "targets the Web Content Accessibility Guidelines (WCAG) 2.2 at level AA", for both the public-facing site and the administrative interface, according to the project’s accessibility statement. Older tutorials and drupal.org nodes still say 2.1 AA; the current statement is 2.2 AA. Two consequences matter to a themer:

  • Significant core changes pass an accessibility gate. The gate blocks changes that carry serious accessibility problems. The statement is explicit that passing the gate is a safeguard, not a complete conformance assessment, so core themes are a good starting point but not a certificate for the finished site.

  • Contributed modules and custom themes are outside the gate. Accessibility of the meridian theme, of its components and of every contributed block or widget depends on the project. A site’s own conformance claim has to be tested on the site.

Core’s front-end tooling reflects the target. Core ships ARIA-aware JavaScript helpers (the two covered below), the visually-hidden and focusable utility classes, and Olivero’s accessible markup patterns. Core’s own accessibility checks run in its automated test suite; a theme can run the same engine, axe-core, in its own pipeline (see Testing in the pipeline).

Where the roles meet

Role Accessibility responsibility

Themer

Landmarks, skip links, focus styles, colour contrast, component markup, keyboard operation of scripted widgets

Site builder

Heading order in templates and Views, alt text fields required on image fields, text formats that keep tables and headings valid

Content editor

Alt text, link text, heading order, captions, transcripts, usually helped by Editoria11y

Developer

ARIA live announcements, focus management in AJAX and dialogs, accessible form errors

A skip link is the first focusable element on the page. It lets a keyboard user jump past the header and navigation to the content. Olivero’s html.html.twig renders it directly after the opening of the body, before page_top:

<a href="#main-content" class="visually-hidden focusable skip-link">
  {{ 'Skip to main content'|t }}
</a>

Its target is an anchor in page.html.twig, inside the main content region:

<div class="main-content">
  <a id="main-content" tabindex="-1"></a>
  {{ page.content }}
</div>

The pieces fit together as follows:

  • visually-hidden removes the link from the visual layout while leaving it in the accessibility tree, and focusable makes the same element visible again while it has keyboard focus. Together they give the classic "appears only when tabbed to" behaviour. Olivero then positions and styles the focused link in css/components/skip-link.css.

  • tabindex="-1" on the target anchor makes it programmatically focusable, so that activating the link moves keyboard focus to that position and not only the scroll position. Without it some browsers scroll but leave focus at the link, and the next Tab press returns to the header.

  • The visible text goes through |t, so it is translated with the rest of the interface (see Multilingual).

The commonest defects are all in the theme: deleting the main-content anchor from page.html.twig because it looks empty (the skip link then points nowhere), moving the main content into a different region so the anchor is no longer immediately before it, giving the focused link display: none-style hiding that also hides it from keyboard users, and removing the focus outline without replacing it. A theme generated with Starterkit (see Starterkit and Subthemes) copies the starter theme’s own templates; check that both halves of the skip link survive any template override. Because Olivero leaves core in Drupal 12, a theme must carry its own copy of the pattern rather than depend on Olivero’s templates or CSS.

For a page with several landmark groups, a site may offer more than one skip link, for example "Skip to main content" and "Skip to search". Each target needs its own id and tabindex="-1".

A meridian variant with two skip links:

{# templates/layout/html.html.twig (extract) #}
<a href="#main-content" class="visually-hidden focusable skip-link">
  {{ 'Skip to main content'|t }}
</a>
<a href="#museum-search" class="visually-hidden focusable skip-link">
  {{ 'Skip to collection search'|t }}
</a>

Source: Twig in Drupal and the Olivero templates in Drupal core: Olivero layout templates

Focus styles and keyboard operation

WCAG 2.2 AA has two criteria that bear directly on focus styling: a visible keyboard focus indicator must exist (success criterion 2.4.7), and a focused element must not be entirely hidden by author-created content such as a sticky header (2.4.11, Focus Not Obscured, new in 2.2). Target size (2.5.8, 24 by 24 CSS pixels at minimum) and dragging alternatives (2.5.7) are also new in 2.2 and affect small icon buttons and drag handles in themes.

/* css/base/focus.css in the meridian theme */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--meridian-focus, #1a4fd6);
  outline-offset: 2px;
}

/* Keep focused items clear of the sticky header (WCAG 2.2, 2.4.11). */
:target,
[tabindex="-1"]:focus {
  scroll-margin-block-start: 5rem;
}

@media (forced-colors: active) {
  :focus-visible {
    outline-color: Highlight;
  }
}

:focus-visible shows the indicator for keyboard focus and hides it for pointer clicks on most elements. Never write outline: none on :focus without a replacement, and test windows high-contrast or forced-colours mode, where background colours disappear and only outlines and borders remain. The contrast of the indicator against its surroundings should be at least 3:1.

Attaching the stylesheet goes through the theme’s library, as for any asset:

# meridian.libraries.yml
global-styling:
  version: VERSION
  css:
    base:
      css/base/focus.css: {}
    theme:
      css/components/skip-link.css: {}

Scripted widgets need the same care. A custom exhibition filter or a gallery lightbox built from a div must provide a role, a name, keyboard operation and a managed focus; native elements (button, details, dialog) give most of that for free and are preferable. Single Directory Components (see Single Directory Components) are a good unit for this: the component template holds the semantics, and its .js file attaches the behaviour.

Announcing changes: Drupal.announce

When JavaScript changes the page without a navigation, a sighted user sees the change and a screen reader user does not. Drupal.announce(text, priority), defined in core/misc/announce.js, writes the text into an ARIA live region so that assistive technology reads it.

How it works inside core:

  • A behavior named drupalAnnounce creates one div with the id drupal-live-announce, the class visually-hidden, aria-live="polite" and aria-busy="false", and appends it to the body.

  • Drupal.announce() pushes the text and priority onto an internal queue and then calls a debounced function with a 200 ms delay. Several announcements made in quick succession are joined, separated by newlines, and written as one text node, because a live region reads only the text that is present when it is evaluated.

  • If any queued announcement has the priority 'assertive', the whole group is announced as assertive; otherwise the default is 'polite'. Assertive announcements interrupt the screen reader, so they are for errors and urgent state.

  • The live region is cleared before each write so that a repeated string is still read.

  • The text should already be translated: pass it through Drupal.t() first.

A theme or module declares the dependency in its library:

# meridian.libraries.yml
exhibition-filter:
  version: VERSION
  js:
    js/exhibition-filter.js: {}
  dependencies:
    - core/drupal
    - core/once
    - core/drupal.announce
    - core/drupal.debounce

and uses it in a behavior. The exhibition filter below shows or hides teasers and reports the result count:

/**
 * @file
 * Filters exhibition teasers on the Meridian "What's on" page.
 */
((Drupal, once) => {
  Drupal.behaviors.meridianExhibitionFilter = {
    attach(context) {
      once('meridian-filter', '[data-meridian-filter]', context).forEach((select) => {
        const list = document.querySelector('[data-meridian-exhibitions]');
        select.addEventListener('change', () => {
          let shown = 0;
          list.querySelectorAll('[data-gallery]').forEach((item) => {
            const match = !select.value || item.dataset.gallery === select.value;
            item.hidden = !match;
            if (match) {
              shown += 1;
            }
          });
          Drupal.announce(
            Drupal.formatPlural(
              shown,
              '1 exhibition shown',
              '@count exhibitions shown',
            ),
          );
        });
      });
    },
  };
})(Drupal, once);

Two pitfalls recur. Announcing on every keystroke floods the user; announce the settled result (the debounce helps but does not replace a considered design). And an announcement does not move focus: when a dialog opens or content is replaced, focus must still be placed deliberately, which is what the tabbing manager and the Drupal Ajax framework help with. For Ajax in particular, see AJAX and HTMX.

Constraining focus: the tabbing manager

Drupal.tabbingManager, defined in core/misc/tabbingmanager.js and available through the library core/drupal.tabbingmanager, manages modifications to the page’s tabbing order. It is a stack of "tabbing contexts". Only the context at the top of the stack is active: the elements it lists are reachable by the Tab key, and the elements it disables are not. The library depends on core/jquery, core/drupal and core/tabbable (the tabbable package computes which elements can currently receive focus), so this part of the API still takes jQuery objects, selectors or elements.

API Purpose

Drupal.tabbingManager.constrain(elements, { trapFocus })

Pushes a new context that makes only elements tabbable. It returns a TabbingContext. With trapFocus: true, tabbing past the last element returns to the first, and Shift+Tab on the first element wraps to the last. With the default false the user can tab out of the page into the browser chrome.

tabbingContext.release()

Marks the context released and returns to the previous one. A released context can never be activated again.

drupalTabbingConstrained, drupalTabbingContextActivated, drupalTabbingContextDeactivated, drupalTabbingContextReleased

Events that other scripts can listen for.

Core uses it for the Off-canvas tray, the contextual toolbar and the toolbar. A custom overlay, such as the Meridian gallery lightbox, uses it the same way:

((Drupal, once) => {
  Drupal.behaviors.meridianLightbox = {
    attach(context) {
      once('meridian-lightbox', '[data-meridian-lightbox-open]', context).forEach((button) => {
        button.addEventListener('click', () => {
          const dialog = document.getElementById(button.getAttribute('aria-controls'));
          dialog.hidden = false;

          const tabbingContext = Drupal.tabbingManager.constrain(dialog, { trapFocus: true });
          dialog.querySelector('[data-meridian-lightbox-close]').focus();
          Drupal.announce(Drupal.t('Gallery opened'));

          dialog.querySelector('[data-meridian-lightbox-close]').addEventListener('click', () => {
            dialog.hidden = true;
            tabbingContext.release();
            button.focus();
          }, { once: true });
        });
      });
    },
  };
})(Drupal, once);

The three habits that make this accessible are: move focus into the dialog when it opens, release the context and return focus to the opener when it closes, and close on the Escape key. The native <dialog> element opened with showModal() provides the first and last of these without any Drupal code and is the better choice for new components; the tabbing manager is most useful when integrating with core UI that already uses it.

Editoria11y in Drupal CMS

Themes make accessible markup possible, but editors decide most of the accessibility of the content. Drupal CMS includes the contributed Editoria11y module (an open-source, multilingual accessibility checker) as part of its "Accessibility Tools" add-on; the recipe that carries it installs the editoria11y and views modules and is named Accessibility Tools. According to the Drupal CMS documentation:

  • An administrator installs the Accessibility Tools add-on from Extend > Recommended add-ons (/admin/modules/browse/recommended).

  • A small widget then appears on content editing forms and on content pages. A blue check means no issues were found; a red flag shows the number of issues or warnings.

  • The sitewide Content Accessibility report at Reports > Content Accessibility (/admin/reports/editoria11y) lists issues across the site’s content.

  • It flags missing or unclear alternative text, skipped or empty headings, unhelpful link text, tables without headers and possible low colour contrast. Some results are warnings that need human review, for example embedded video.

Permissions are separate from the roles in the Meridian scenario: the module provides View Editoria11y Checker, plus permissions for the "ignore" and "mark OK" buttons and for the dashboard reports. It tries to grant the checker to roles with common node edit permissions by default; the Content Editor role is therefore covered, and the Curator and Reviewer roles should be checked on the permissions page (see Users, Roles and Permissions). Editoria11y 3.x supports Drupal 10.3, 11 and 12 (3.0.11 was the current stable release on 2026-10-09); the 2.2.x line supports Drupal 9, 10 and 11.

Drupal CMS is a distribution of recipes, so on a Drupal CMS 2.x site the checker sits beside Canvas, the default editor (see Canvas Visual Page Building and Drupal CMS and Site Templates). On a plain Drupal 11 site the same module is installed with Composer:

composer require drupal/editoria11y
ddev drush en editoria11y -y
ddev drush cr

Two limits are worth stating plainly. An automated checker finds only the problems that can be detected from markup; it cannot judge whether alt text is accurate or a transcript is correct. And it checks rendered content, so a theme that hides focus outlines or sets low-contrast colours is outside what the editor tools can fix. The AI-generated alt text feature of the AI module (see AI Module and Providers) is a drafting aid that an editor still reviews.

Testing in the pipeline

Manual keyboard and screen reader checks stay necessary, but regressions in a theme are cheaper to catch in continuous integration. A theme repository can run axe-core, or Lighthouse’s accessibility category, against the rendered pages of a test site. See End-to-End Testing for browser-test tooling and Automated Testing for the Drupal test types.

Performance: what the theme owns

Performance work in Drupal is shared between the server (page and render caching, covered in Performance and Caching) and the front end. A themer cannot make a cold database query faster, but owns the markup, the CSS and JavaScript that are delivered, the fonts and the images, which largely decide the measurements that visitors and search engines see.

Core Web Vitals

Core Web Vitals are three field metrics, assessed at the 75th percentile of real page loads:

Metric Measures "Good" threshold

Largest Contentful Paint (LCP)

Time until the largest element in the first screen is rendered

2.5 seconds or less

Interaction to Next Paint (INP)

Delay between user input and the next paint, across the visit

200 milliseconds or less

Cumulative Layout Shift (CLS)

Unexpected movement of visible content

0.1 or less

INP replaced First Input Delay as the responsiveness metric in March 2024, so articles that discuss FID are out of date. What each metric means for a Drupal theme:

  • LCP is usually a hero image or a heading. For the Meridian exhibition page it is the hero media. The theme should load that image eagerly with the right size, not lazily, and should not delay it behind blocking CSS or a web font.

  • INP suffers from heavy JavaScript. Drupal behaviors that run on every page, large libraries attached globally, and long tasks in event handlers all raise it. Attach libraries only where they are used (per template or per component with attach_library) and avoid global jQuery plugins.

  • CLS comes from images and embeds without dimensions, late-loading fonts that change the text’s size, and banners inserted above existing content. Drupal image fields output width and height attributes, and responsive images keep them, so the browser can reserve space; custom CSS should set aspect-ratio for embedded video.

Font loading

Fonts are a common cause of both late text rendering and layout shift. The reliable recipe is self-hosted woff2 files, font-display: swap (or optional), a fallback with similar metrics, and a preload for the one or two files that are needed on the first screen:

/* css/base/fonts.css in the meridian theme */
@font-face {
  font-family: "Meridian Sans";
  src: url("../../fonts/meridian-sans-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

body {
  font-family: "Meridian Sans", system-ui, sans-serif;
}

From Drupal 11.4 a library definition can declare fonts and ask Drupal to emit the preload link, so no html_head_alter code is needed. Olivero’s own library uses the same key for its Metropolis fonts:

# meridian.libraries.yml (Drupal 11.4 and later)
global-styling:
  version: VERSION
  css:
    base:
      css/base/fonts.css: {}
  fonts:
    fonts/meridian-sans-regular.woff2:
      preload: true

Preload only the fonts that are certain to be used above the fold; preloading everything competes with the LCP image for bandwidth. Setting preload: false is equivalent to omitting the entry. On Drupal 10.x and 11.0 to 11.3 the fonts key does not exist, so the preload link is added by hand, either in the html.html.twig template or from an html_head attachment. Font preloads need the crossorigin attribute even for same-origin files:

<?php

/**
 * Implements hook_page_attachments_alter() (Drupal 10.x to 11.3 fallback).
 */
function meridian_page_attachments_alter(array &$attachments): void {
  $attachments['#attached']['html_head'][] = [
    [
      '#tag' => 'link',
      '#attributes' => [
        'rel' => 'preload',
        'href' => '/themes/custom/meridian/fonts/meridian-sans-regular.woff2',
        'as' => 'font',
        'type' => 'font/woff2',
        'crossorigin' => 'anonymous',
      ],
    ],
    'meridian_font_regular',
  ];
}

Themes can implement hooks as methods with the #[Hook] attribute from Drupal 11.3 (preprocess hooks from 11.2); the procedural function above is used only because this fallback is for versions that predate that, and only for a site that cannot yet use 11.4. Older books such as Modernizing Drupal 10 Theme Development (2023) were written for Drupal 10.1 and show hand-written preload links, which was then the only option; since 11.4 the fonts key replaces them. Fonts served from a third-party host add a DNS lookup and connection; self-hosting them is also kinder to visitors' privacy.

Image formats and lazy loading

Images are usually the heaviest part of a page. Drupal gives three levers, all configuration rather than code, described in Display Modes and Image Styles and Breakpoints and Responsive Images:

  • Modern formats through image styles. The Image module has a Convert effect that changes the output format. WebP conversion works with the standard GD toolkit. AVIF is supported by the GD toolkit from Drupal 11.2 and depends on the server: PHP’s GD must be built against libgd 2.3.2 or later with libavif, otherwise the option is missing or conversion fails. The "Convert to AVIF" effect (image_convert_avif) therefore usually sits in a style that falls back to WebP. Check the format list on the image toolkit configuration page (/admin/config/media/image-toolkit) of each environment, since a local DDEV container and production can differ. Drupal 11.2.0 had a fatal error on builds of GD without AVIF encoding; it was fixed in 11.2.x, so run a current 11.4 release.

  • Responsive image styles. A responsive image style maps breakpoints to image styles and renders a picture element or a srcset with sizes, so a phone downloads a small file. Define one style per layout slot (hero, teaser, gallery thumbnail), not one per page.

  • Native lazy loading. The image and responsive image formatters have an Image loading setting with lazy (the default) and eager. Lazy loading is right for everything below the first screen, and wrong for the LCP image.

Configuration for the Meridian hero, loaded eagerly, and for the gallery thumbnails, lazily, in a view display export:

# config/sync/core.entity_view_display.node.exhibition.default.yml (extract)
content:
  field_hero_image:
    type: responsive_image
    label: hidden
    settings:
      responsive_image_style: meridian_hero
      image_link: ''
      image_loading:
        attribute: eager
    third_party_settings: {  }
    weight: 0
    region: content
  field_gallery:
    type: responsive_image
    label: hidden
    settings:
      responsive_image_style: meridian_thumbnail
      image_link: file
      image_loading:
        attribute: lazy
    third_party_settings: {  }
    weight: 5
    region: content

Alt text belongs to this topic as well: the image field’s Alt field required setting is the site builder’s guard, and the decorative-image case is handled with an empty alt attribute. A hero used purely as decoration should be rendered by CSS, not as content.

Critical CSS and asset loading

Render-blocking CSS delays LCP. Drupal’s defaults already help: CSS and JavaScript aggregation (Configuration > Development > Performance, or $config['system.performance']['css']['preprocess'] in settings.php) combines files and enables long-lived caching of the aggregates, and the library system loads only the libraries attached to the page. The themer’s tools beyond that are:

  • Split the CSS by component. In a Single Directory Component, the component’s .css is attached only when the component renders, so a page without the exhibition-teaser never downloads its styles.

  • Per-template libraries. Attach a heavy library (the map, the lightbox) from the template that needs it with {{ attach_library('meridian/lightbox') }}.

  • JavaScript in the footer, deferred. Libraries load their scripts at the end of the body by default; header: true moves one to the head, so reserve it for the rare script that must run early.

  • Critical CSS. The approach is to inline the CSS required for the first screen in the head and load the rest without blocking. Core has no switch for this. A build step (a tool in the project’s front-end pipeline, see Front-End Tooling) extracts the above-the-fold rules for the main templates, and the theme outputs them in a style element and loads the full stylesheet with rel="preload" plus an onload switch. Inlined CSS defeats browser caching and must be kept small, and every template needs its own critical set, so it is the last optimisation to adopt, and one to measure before and after. For most Drupal sites, aggregation, component-level CSS and eager hero images give the larger part of the gain.

# meridian.libraries.yml
lightbox:
  version: VERSION
  js:
    js/lightbox.js: {}
  css:
    component:
      css/components/lightbox.css: {}
  dependencies:
    - core/drupal
    - core/once
    - core/drupal.tabbingmanager
    - core/drupal.announce
{# templates/field/field--field-gallery.html.twig (extract) #}
{{ attach_library('meridian/lightbox') }}
<div{{ attributes.addClass('meridian-gallery') }}>
  {% for item in items %}
    {{ item.content }}
  {% endfor %}
</div>

Lighthouse checks

Lighthouse is the audit built into Chromium’s developer tools and available from the command line. It reports a Performance score from lab measurements (including LCP and CLS, plus Total Blocking Time as a proxy for INP, since lab tests cannot measure real interactions) and an Accessibility score from axe-core rules. Both are useful and both are partial:

  • The Accessibility score covers only what can be detected automatically, which is a minority of WCAG criteria. A 100 does not mean the page is accessible, and keyboard, screen reader and zoom testing are still needed.

  • The Performance score is a lab result on one device profile. Real visitors are measured by field data (the Chrome UX Report, or the Core Web Vitals report in Search Console), which can differ widely.

  • Test with Drupal’s caches warmed and as the anonymous user, since logged-in pages bypass the page cache and look slower. Remember that Twig debugging and disabled aggregation in a development settings.local.php distort the numbers (see Twig Templates and Debugging); measure a production-like build.

A repeatable command against the DDEV site, with the result stored as JSON for comparison between commits:

ddev drush cache:rebuild
npx lighthouse@12 https://meridian.ddev.site/exhibitions \
  --only-categories=performance,accessibility \
  --output=json --output-path=./lighthouse-exhibitions.json \
  --chrome-flags="--headless=new --ignore-certificate-errors"

A short checklist for each release of the theme:

  1. Keyboard only: reach every control, see the focus ring, skip link works, no keyboard trap, Escape closes overlays.

  2. Zoom to 200 percent and 400 percent wide: no loss of content or horizontal scrolling for text content.

  3. Screen reader: landmarks and headings make sense, dynamic changes are announced.

  4. Editoria11y report is clean for the demo content, and colour contrast of the theme’s palette meets AA.

  5. Lighthouse: LCP element is the intended one and is not lazy; no layout shift from fonts or banners; scripts are attached only where used.

flowchart LR A[Template change] --> B[Keyboard and focus review] B --> C[axe-core or Lighthouse accessibility] C --> D[Lighthouse performance on warmed cache] D --> E{LCP, INP and CLS in range?} E -- no --> F[Fix fonts, images, libraries] F --> D E -- yes --> G[Release the theme]

Common errors and pitfalls

  • Removing or hiding the skip link target. Deleting the empty main-content anchor, or moving the main region, breaks the link silently.

  • outline: none without a replacement, or a focus indicator whose contrast is below 3:1. A sticky header can also cover the focused element (WCAG 2.2, 2.4.11).

  • Announcing everything, or announcing before translating. Pass strings through Drupal.t() and Drupal.formatPlural(); keep announcements short and meaningful.

  • Constraining tabbing and forgetting to release. An unreleased TabbingContext leaves the rest of the page unreachable by keyboard until reload. Release in every close path, including error paths.

  • Lazy loading the LCP image, the commonest cause of a poor LCP on a Drupal page that otherwise looks fast. Set the hero’s display to eager.

  • Preloading fonts without crossorigin (hand-written links), so the font downloads twice; preloading too many fonts.

  • AVIF without server support. Configure a WebP fallback, and test each environment.

  • Trusting a Lighthouse 100. It is a lab test with automated rules only.

  • Copying Olivero’s CSS and templates verbatim. They leave core in Drupal 12; copy the patterns into the theme’s own files, or start from the Starterkit theme.

Version notes

Version Change

Drupal 10.0

Starterkit replaces Classy and Stable as the way to start a theme

Drupal 10.3

Single Directory Components are part of core, so component CSS and JS are attached with the component

Drupal 11.2

GD toolkit supports AVIF; theme preprocess hooks can be OOP

Drupal 11.3

Themes can implement OOP hooks

Drupal 11.4

Library definitions support the fonts key with preload

Drupal 12 (planned week of 2026-12-07)

Olivero, Claro and Stable 9 are removed from core