Drupal: Theme Info Files and Regions

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.

Every Drupal theme starts with one small file, THEME.info.yml, where THEME is the theme’s machine name. The file tells Drupal that a directory is a theme, which theme it builds on, which core versions it works with, which regions it offers to the Block layout page, and which CSS and JavaScript libraries it loads, overrides or extends. This page explains each key of that file, the defaults core fills in, what happens internally when a theme is discovered and activated, how regions become Twig variables, what the hidden regions are, and how to install a theme and make it the default for visitors.

It was written against Drupal 11.4.x, with Drush 13 or 14 and PHP 8.3 or later, and the facts were checked against the official documentation and the core source on 2026-10-30. The running example is the Meridian Museum and its theme meridian. Drupal 12.0.0 is scheduled for the week of 2026-12-07 and the end of life of Drupal 10 for 2026-12-09, so both are described as upcoming.

Who works with theme info files

  • Site builders install themes, choose the default and administration themes and place blocks into the regions a theme offers.

  • Themers write and maintain the file, declare regions and attach libraries.

  • Developers read the processed values ($theme->info, listVisibleRegions()) and alter them with hook_system_info_alter().

  • Administrators decide which themes are installed on a production site and keep the code under Composer.

Where the file lives and what makes a theme

A theme is a directory below themes/ (custom themes in themes/custom/, downloaded themes in themes/contrib/) that contains THEME.info.yml. The directory name and the file name before .info.yml must agree, and the name becomes the theme’s machine name used in the system.theme configuration, in Twig namespaces, in library names (meridian/global) and in block configuration (theme: meridian). Use lowercase letters, digits and underscores, start with a letter, and do not reuse the name of a module, because themes and modules share the same namespace for hook implementations and libraries.

For the Meridian Museum the minimal layout is:

themes/custom/meridian/
  meridian.info.yml
  meridian.libraries.yml
  meridian.theme            # optional: preprocess functions
  logo.svg                  # optional: default logo
  screenshot.png            # optional: image on the Appearance page
  css/global.css
  js/menu.js
  templates/page.html.twig  # optional: overrides

The recommended way to create that skeleton is the Starterkit generator described in Starterkit and sub-themes; the sections below explain what the generated file contains. The overall render pipeline that uses the result is in the theming overview. Modules use a similar file with other keys, described in module anatomy and the info file.

A complete example

name: Meridian
type: theme
description: 'Theme of the Meridian Museum: exhibitions, events and the collection.'
package: Meridian
core_version_requirement: ^11.3
base theme: false
logo: logo.svg
screenshot: screenshot.png
generator: 'starterkit_theme:11.4.0'

libraries:
  - meridian/global
  - meridian/menu

libraries-override:
  system/base:
    css:
      component:
        css/components/hidden.module.css: false
  core/drupal.dialog: meridian/dialog

libraries-extend:
  core/drupal.dialog.ajax:
    - meridian/dialog-ajax
  views/views.module:
    - meridian/views-grid

ckeditor5-stylesheets:
  - css/ckeditor5-content.css

regions:
  header: 'Header'
  primary_menu: 'Primary menu'
  highlighted: 'Highlighted'
  breadcrumb: 'Breadcrumb'
  content: 'Content'
  sidebar_first: 'Sidebar'
  footer_top: 'Footer top'
  footer_bottom: 'Footer bottom'

The values above are illustrative: the library names (meridian/global and so on) must exist in meridian.libraries.yml, which is explained in libraries and assets. The sections that follow go through the keys in the order a themer meets them.

Required keys

A theme appears on the Appearance page only when its file has the four keys name, type, base theme and core_version_requirement. Core does not guess any of them.

Key Value Notes

name

A human-readable label.

Shown on the Appearance page and in the Block layout tabs. Quote it when it contains a colon.

type

The literal theme.

Case-sensitive. A module uses module and a profile profile; any other value hides the directory from the theme list.

base theme

The machine name of the parent theme, or false.

Required. A missing key makes the info parser throw an exception that points at the change record that introduced the requirement. The key contains a space, so it is written base theme: exactly.

core_version_requirement

A Composer-style constraint on core.

Required since Drupal 9. The old core: 8.x key is not valid any more.

name

The label is plain text. It is the only place the end user sees the theme’s name, so write it for editors (Meridian), not for developers (meridian_theme_v2).

type

type: theme is what makes the extension discovery treat the directory as a theme. If a theme and a module in the same site have the same machine name, only one of them is installable, so keep the names distinct (the museum’s module is meridian_collection, its theme meridian).

base theme

The base theme gives the new theme its templates, libraries and regions. Resolution walks up the chain: if a theme extends stable9, Drupal looks for a template in that theme, then in stable9, then in the module that provides the theme hook. Libraries are resolved the same way, and a child theme inherits the libraries-override and libraries-extend rules of its ancestors.

  • Use the machine name, for example base theme: stable9. A base theme must be present in the codebase, and it is installed as a dependency of the child.

  • base theme: false declares a theme without any base theme. Core removes the key while it processes the file, so the theme inherits no templates and no libraries, not even the markup of Stable 9. This is the choice of the Starterkit theme and of fully custom themes that supply every template themselves.

  • Changing the base theme of a theme that is already installed requires reinstalling the theme, because the base theme is recorded as a dependency while the theme is being installed.

  • The base theme is also added to the theme’s dependencies internally, so a missing base theme gives the dependency error "Theme X requires theme Y".

Which base theme to choose depends on the Drupal version. Classy and Stable were removed in Drupal 10.0, and Starterkit (php core/scripts/drupal generate-theme) replaced the habit of extending Classy. In 11.4 core still ships Olivero and Claro for the front end and the administration interface, and Stable 9 as a base theme that carries the markup of Drupal 9. All three are scheduled to leave core in Drupal 12 (Default Admin, based on Gin, becomes the administration theme), so a new theme should not extend Olivero or Claro, and should extend stable9 only if it needs that markup and the team accepts to carry it as a contributed project afterwards. The usual advice is to generate a standalone theme from Starterkit with base theme: false. The upgrade angle is covered in Preparing for Drupal 12.

Older tutorials and blog posts show base theme: classy. That theme no longer exists in Drupal 10 or 11, so those examples fail with a missing-dependency error.

core_version_requirement

The value is a Composer semantic-version constraint that core evaluates against \Drupal::VERSION. Typical values:

  • ^10.3 || ^11: compatible with Drupal 10.3 and later and with every 11.x. Common for contributed themes that straddle the upgrade.

  • ^11: any Drupal 11. This is what Starterkit writes (the caret on the current major version).

  • ^11.3: Drupal 11.3 or later 11.x; use it when the theme relies on a newer API, for example OOP hooks in themes (11.3).

  • >=10.3: open-ended; avoid it, because it also claims compatibility with Drupal 12 and later, which has not been tested.

If the constraint does not match, the theme is listed on the Appearance page as incompatible and cannot be installed. Drupal 12 will refuse themes that do not allow ^12, so contributed themes should add it as soon as they pass their tests there. The retired core: 8.x key that older material shows is obsolete.

Descriptive and dependency keys

description is the sentence shown under the theme on the Appearance page. package groups extensions; for themes it is mostly informational. version is written by the drupal.org packaging script for contributed projects, so do not set it on a project hosted there; Starterkit sets 1.0.0 on generated themes. dependencies lists modules and themes, other than the base theme, that must be present, in the form drupal:views or paragraphs:paragraphs, with an optional version constraint in parentheses. php sets a minimum PHP version and defaults to the one core requires. hidden: true removes the theme from the Appearance page so it cannot be switched on by an editor; Starterkit uses it for the starter theme itself, and the generator removes the key from the copy it writes. engine defaults to twig and almost never needs setting.

features lists the options shown on the theme’s settings page (favicon, logo, node_user_picture, comment_user_picture, comment_user_verification). Core’s defaults include all five, so you only list features to shrink the set. Theme settings and configurable themes are covered in theme settings.

The deprecated stylesheets-remove key and the old core key belong to the Drupal 8 era. Use libraries-override instead of the first, and core_version_requirement instead of the second.

Regions

A region is a named area of the page into which blocks can be placed. The theme declares regions in the regions key as a map from machine name to label.

regions:
  header: 'Header'
  primary_menu: 'Primary menu'
  highlighted: 'Highlighted'
  breadcrumb: 'Breadcrumb'
  content: 'Content'
  sidebar_first: 'Sidebar'
  footer_top: 'Footer top'
  footer_bottom: 'Footer bottom'

Rules that follow from core’s behaviour:

  • The key is the machine name, a lowercase identifier with underscores; the value is the label shown on the Block layout page. Do not put a dash in front of the key: regions is a map, not a list.

  • A content region is required. It holds the main content block and the status messages, so a theme without it breaks the page.

  • If the theme declares no regions key at all, core supplies a default set: sidebar_first, sidebar_second, content, header, primary_menu, secondary_menu, footer, highlighted, help and breadcrumb, together with the hidden page_top and page_bottom. The default page.html.twig prints them in a fixed order, so themes usually declare their own list to control the order.

  • Once a theme declares regions, the default set no longer applies. Every region you want must be listed, and blocks that were placed in a region that is no longer declared are disabled and shown in the "Disabled" list of the Block layout page until you assign them again.

  • Region names are part of the block configuration (region: footer_top in block.block.meridian_branding.yml), so renaming a region after launch means touching every block placed in it and re-exporting the configuration.

From a region to the page

Each region becomes a variable in the page template. The region header is available as {{ page.header }}, the region footer_top as {{ page.footer_top }}. The value is a render array; Drupal prints nothing when the region has no blocks, so wrap the region in an if to avoid empty containers.

{# themes/custom/meridian/templates/layout/page.html.twig #}
<header class="site-header" role="banner">
  {{ page.header }}
  {% if page.primary_menu %}
    <nav class="primary-menu" aria-label="{{ 'Main navigation'|t }}">
      {{ page.primary_menu }}
    </nav>
  {% endif %}
</header>

{% if page.highlighted %}
  <div class="highlighted">{{ page.highlighted }}</div>
{% endif %}

{{ page.breadcrumb }}

<main id="main-content" class="layout-container">
  {% if page.sidebar_first %}
    <aside class="sidebar" aria-label="{{ 'Sidebar'|t }}">{{ page.sidebar_first }}</aside>
  {% endif %}
  <div class="main-content">{{ page.content }}</div>
</main>

<footer class="site-footer" role="contentinfo">
  {{ page.footer_top }}
  {{ page.footer_bottom }}
</footer>

A region declared in the file but never printed in any template is a common mistake: editors place a block into it on the Block layout page, the configuration is saved, and nothing appears on the site. The reverse mistake, printing {{ page.sidebar_second }} without declaring the region, prints nothing as well.

What happens when an editor places a block

The Block layout page (admin/structure/block) has one tab per installed theme. For the selected theme it lists the visible regions from the theme object, and "Place block" saves a block configuration entity whose theme and region properties point to the theme and the region. Placement is therefore per theme: installing a second theme starts with an empty layout. The page and the visibility conditions are described in blocks and the Block layout page. Because the placements are configuration, they travel with the configuration export described in configuration management.

Hidden regions

Hidden regions exist in the theme but do not appear on the Block layout page, so an editor cannot place blocks into them. Core reserves two of them, page_top and page_bottom, for modules that need to add markup from outside the block system: the administration toolbar, the contextual-links support, analytics snippets and similar. Core adds them to every theme’s regions_hidden list when it processes the theme information (in the system_info_alter hook of the System module), so a theme does not need to declare them. Keep the corresponding Twig variables in html.html.twig:

{# themes/custom/meridian/templates/layout/html.html.twig (excerpt) #}
<body{{ attributes }}>
  <a href="#main-content" class="visually-hidden focusable skip-link">
    {{ 'Skip to main content'|t }}
  </a>
  {{ page_top }}
  {{ page }}
  {{ page_bottom }}
  <js-bottom-placeholder token="{{ placeholder_token }}">
</body>

If a theme removes {{ page_top }} or {{ page_bottom }}, the toolbar for administrators or an analytics module’s output silently disappears, so treat those two variables as part of the contract of html.html.twig.

A theme can hide additional regions with the regions_hidden key, a list of machine names. The official page describes it as the way to remove regions inherited from a base theme from the Block layout page:

regions_hidden:
  - sidebar_second

Internally the Theme extension object keeps every region and exposes two methods: listAllRegions() and listVisibleRegions(), the latter leaving out the entries of regions_hidden. Since Drupal 11.4 these replace the procedural system_region_list() function, system_default_region() and the constants REGIONS_ALL and REGIONS_VISIBLE, which are deprecated and scheduled for removal in 13.0. Custom code obtains the object from the theme_handler service.

<?php

namespace Drupal\meridian_collection;

use Drupal\Core\Extension\ThemeHandlerInterface;

/**
 * Lists the regions an editor may use in the museum's front-end theme.
 */
final class RegionLister {

  public function __construct(private readonly ThemeHandlerInterface $themeHandler) {}

  /**
   * Returns machine name => label for the regions visible on Block layout.
   */
  public function visibleRegions(string $theme = 'meridian'): array {
    return $this->themeHandler->getTheme($theme)->listVisibleRegions();
  }

}

Code written for earlier versions that calls system_region_list($theme, REGIONS_VISIBLE) keeps working in 11.4 but logs a deprecation notice; update it before Drupal 13.

Libraries

libraries

libraries lists the asset libraries the theme attaches to every page where it is the active theme. Each entry is extension/library-name, and the library is defined in THEME.libraries.yml.

# meridian.info.yml
libraries:
  - meridian/global
  - meridian/menu
# meridian.libraries.yml
global:
  version: VERSION
  css:
    theme:
      css/global.css: {}
  dependencies:
    - core/normalize

menu:
  js:
    js/menu.js: {}
  dependencies:
    - core/drupal
    - core/once

Attach only what every page needs here. Libraries needed by one template or one component are better attached with {{ attach_library('meridian/exhibition-teaser') }} in that template, so unused pages do not download them; the same holds for Single Directory Components, which bring their own assets (see Single Directory Components). With CSS and JavaScript aggregation on, core merges and fingerprints the files, as described in performance and caching.

libraries-override

libraries-override lets a theme replace or remove assets that core, a module or a base theme declares. The keys are extension/library names. Three forms exist:

  • a map under css or js (with the category base, layout, component, state or theme for CSS) that replaces one file with another file, or removes it with false

  • a string that replaces the whole library with a library of the theme

  • false to remove the whole library

libraries-override:
  # Replace one file of a library with the theme's own copy.
  views/views.module:
    css:
      component:
        css/views.module.css: css/overrides/views-museum.css
  # Drop one core stylesheet.
  system/base:
    css:
      component:
        css/components/hidden.module.css: false
  # Replace the whole dialog library.
  core/drupal.dialog: meridian/dialog
  # Remove a library completely.
  core/drupal.vertical-tabs: false

Points to remember:

  • File paths are relative to the original library’s own directory (core/modules/system for system/base, as in the second example); a path starting with a slash is relative to the web root. The replacement path is relative to the theme.

  • Overriding works for the active theme and its base themes, so a sub-theme can undo an override of its parent only by overriding the same library again.

  • Removing a core stylesheet can break the behaviour that depends on it. Test keyboard focus, dialogs and the administration overlay after removing anything. Removing a JavaScript file removes the behaviours it defines.

  • Override rules are part of the library definition that is cached, so after changing the file clear the caches with drush cache:rebuild.

libraries-extend

libraries-extend attaches additional libraries whenever a given library is attached, without touching the original. It is the safe way to add the museum’s own styling to a core widget.

libraries-extend:
  core/drupal.dialog.ajax:
    - meridian/dialog-ajax
  views/views.module:
    - meridian/views-grid

Prefer libraries-extend to libraries-override whenever adding is enough: extending keeps working when core changes the original files, whereas an overridden file stays frozen at the version you copied.

Logo, screenshot and editor stylesheets

  • logo is a path relative to the info file. When the key is missing, core uses logo.svg in the theme’s root directory if it exists. The site branding block and the theme settings page use it. A logo uploaded on the theme settings page takes precedence.

  • screenshot is the image on the Appearance page and defaults to screenshot.png in the theme root; the documentation recommends 588 by 438 pixels. Core prefixes the path with the theme’s path.

  • ckeditor5-stylesheets lists the stylesheets that CKEditor 5 loads into its editing frame so that editors see content styled like the live site. Entries are theme-relative paths or full URLs. The key has existed since Drupal 9.3 and is the only editor key that matters now: CKEditor 4 was removed in Drupal 10.0, so the older ckeditor_stylesheets key that books and tutorials mention has no effect.

logo: logo.svg
screenshot: screenshot.png
ckeditor5-stylesheets:
  - css/ckeditor5-content.css

Do not load fonts from third-party CDNs in the editor stylesheet unless the site’s privacy policy allows it; remote URLs are requested from the editor’s browser. Text formats and the editor toolbar are covered in text formats and CKEditor 5.

The generator key

generator records where a theme came from. When the Starterkit command copies a starter theme, it writes the key in the form starterkit_theme:11.4.0: the machine name of the source theme, a colon and the version of core that was running. For development snapshots the version is followed by a commit hash. The value is informational; core does not read it. Its purpose is to let a developer later compare the generated theme with the starter it was derived from and see which upstream changes (new templates, changed libraries) are missing.

# Generate the museum's theme from the core Starterkit theme.
php web/core/scripts/drupal generate-theme meridian \
  --name "Meridian" \
  --description "Theme of the Meridian Museum" \
  --path themes/custom

grep generator web/themes/custom/meridian/meridian.info.yml

The generated file also gets core_version_requirement set to the caret constraint on the running major version (^11) and version: 1.0.0, and the hidden key is removed. Leave generator in place when you keep the theme in sync with Starterkit, and delete it only when you no longer intend to compare. The command and its starter configuration file THEME.starterkit.yml are explained in Starterkit and sub-themes.

How core processes the file

Understanding the processing explains most surprises, such as a changed regions list that is not visible.

flowchart TD A["meridian.info.yml on disk"] --> B["Extension discovery finds the theme"] B --> C["Info parser reads the YAML and checks the required keys"] C --> D["Defaults are merged: regions, features, screenshot, libraries"] D --> E["hook_system_info_alter adds page_top and page_bottom to regions_hidden"] E --> F["Theme object: listAllRegions and listVisibleRegions"] F --> G["Appearance page and Block layout"] F --> H["ActiveTheme built on request: logo, regions, base themes, library rules"] H --> I["Twig renders page.html.twig with one variable per region"]

The steps in prose:

  1. Discovery: core scans themes/, core/themes/ and the profile directories for *.info.yml files and keeps the ones with type: theme.

  2. Parsing: the YAML is parsed and cached with the extension list. A missing base theme raises an exception; a base theme: false is removed. The base theme, when present, is added to the dependencies.

  3. Defaults: missing keys receive core’s defaults (Twig engine, the default regions, features, screenshot.png, empty library lists). The screenshot path is prefixed with the theme path.

  4. Alter: hook_system_info_alter() implementations can change the information of any extension. The System module uses it to hide page_top and page_bottom; a custom module can do the same to adjust another theme, though changing someone else’s theme in code is usually a sign that a sub-theme is the better tool.

  5. Activation: when a request is rendered, the theme negotiator picks the theme and core builds an active theme object with the logo (the declared logo, or logo.svg in the theme root), the regions, the chain of base themes and the merged library override and extend rules of the whole chain.

Because steps 1 to 3 are cached, a change to the info file is not seen until the cache is rebuilt. After editing the file run drush cache:rebuild (or visit admin/config/development/performance and clear all caches), and clear the browser cache when the logo or screenshot looks stale. With Twig debugging enabled, described in Twig templates and debugging, template changes appear without a rebuild, but info file changes still need one.

Installing a theme and setting the default

Code is added with Composer; the user interface no longer uploads themes (module and theme upload from the UI was removed in 10.4 and 11.0). For a contributed theme:

composer require drupal/honey:^1.0

A custom theme such as meridian is committed to the project repository below web/themes/custom/. Then either use the user interface or Drush.

With the Appearance page

  1. Go to Appearance (admin/appearance).

  2. Find the theme under Uninstalled themes and choose Install and set as default. Install alone makes the theme available (for example for the Block layout page) without changing what visitors see.

  3. For the administration pages, choose the Administration theme in the section further down, or on admin/appearance, and leave it on the default if the front-end theme is not suitable for editing. See the administration interface and admin themes.

  4. Uninstall themes that are not used from the same page. A theme that is the default, the administration theme or the base theme of an installed theme cannot be uninstalled.

With Drush

# Install the theme (aliases: theme:enable, then, thin).
drush theme:install meridian

# Make it the default theme for visitors.
drush config:set system.theme default meridian -y

# Optionally choose the administration theme.
drush config:set system.theme admin claro -y

drush cache:rebuild

The default and administration themes are stored in the system.theme configuration object, so they are exported with the rest of the site configuration:

# config/sync/system.theme.yml
admin: claro
default: meridian

The list of installed themes is kept in core.extension (the theme: map), which means that a deployment that imports configuration installs the theme on the target site; the code must be deployed first. Block placements and theme settings live in block.block.* and meridian.settings configuration. See configuration management and Drush for the deployment commands.

Security considerations

  • Install themes only from sources you trust and keep contributed themes updated like any other project; the security advisory process covers themes. See updates, Composer and security advisories.

  • Remote stylesheet URLs in ckeditor5-stylesheets and external scripts attached through libraries (type: external) send visitor or editor requests to third parties and widen the content-security-policy needed. Prefer local copies.

  • libraries-override can remove core JavaScript that enforces behaviour such as form-state handling or dialog focus management. Removing it never grants access, but it can disable client-side safeguards.

Common errors and pitfalls

Symptom Cause and fix

The theme is missing from the Appearance page

The file name does not match the directory, type is not theme, a required key is missing, or the constraint in core_version_requirement does not match the core version. Rebuild the cache after fixing.

"Missing required key base theme" exception

The key is absent. Add base theme: false or the name of a parent theme.

A base theme error such as classy not found

The example was written for Drupal 8 or 9. Classy and Stable were removed in Drupal 10.0; use Starterkit or stable9.

Blocks vanished after editing regions

Blocks in undeclared regions are disabled. Reassign them on the Block layout page, or restore the region names.

A block is placed but nothing is shown

The region is declared but not printed in page.html.twig (or the template override uses another name).

The toolbar or analytics disappeared

page_top or page_bottom was removed from html.html.twig.

A tab key inside the YAML

YAML forbids tabs. Indent with two spaces; a tab produces a parse error that stops the theme from loading.

Quote problems in description

A colon followed by a space inside an unquoted value ends the key. Quote strings containing a colon, as in description: 'Theme of the Meridian Museum: exhibitions and events'.

Changes to libraries or libraries-override have no effect

The info file and library definitions are cached. Run drush cache:rebuild.

CKEditor does not use the theme’s styling

ckeditor_stylesheets (CKEditor 4) is ignored; use ckeditor5-stylesheets, and check the text format uses CKEditor 5.

A theme cannot be uninstalled

It is the default or administration theme, or another installed theme extends it. Change those first.

Version notes

  • Drupal 8: core: 8.x identified compatible versions; core_version_requirement arrived in 8.7.7 and became mandatory in Drupal 9. logo was added in 8.6 and dependencies in 8.9.

  • Drupal 9.3: ckeditor5-stylesheets added; Drupal 10.0 removed CKEditor 4, Classy and Stable (Stable 9 remained) and made Starterkit the way to start a theme.

  • Drupal 11.3: themes can implement hooks as methods of classes (preprocess hooks from 11.2); a theme that uses this declares core_version_requirement: ^11.3 or later.

  • Drupal 11.4: listAllRegions() and listVisibleRegions() on the Theme object replace system_region_list() and the REGIONS_* constants, which are deprecated.

  • Drupal 12 (scheduled for the week of 2026-12-07): Claro, Olivero and Stable 9 are removed from core, so themes that name one of them as their base theme need a contributed project or a rewrite. The Drupal 10 end of life is 2026-12-09, so existing Drupal 10 sites should move to 11.3 or later before then.

Where books and older tutorials are outdated

  • Modernizing Drupal 10 Theme Development (2023) and Drupal 10 Masterclass (2023) were written for Drupal 10.0 and 10.1. The keys on this page still apply, but advice to extend Olivero, Claro or Stable 9 does not survive Drupal 12, where those themes are removed. Treat base theme: false or a Starterkit-generated theme as the starting point.

  • Material from the Drupal 8 era still shows core: 8.x, ckeditor_stylesheets and stylesheets-remove. The first is invalid, the second belongs to CKEditor 4, and the third was replaced by libraries-override.

  • Drupal 10 Development Cookbook (2023) uses drush theme:enable; it still works, and theme:install is the primary name of the command with theme:enable as an alias.