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 withhook_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 |
|---|---|---|
|
A human-readable label. |
Shown on the Appearance page and in the Block layout tabs. Quote it when it contains a colon. |
|
The literal |
Case-sensitive. A module uses |
|
The machine name of the parent theme, or |
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 |
|
A Composer-style constraint on core. |
Required since Drupal 9. The old |
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: falsedeclares 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
dependenciesinternally, 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'
Source: Adding regions to a theme
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:
regionsis a map, not a list. -
A
contentregion 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
regionskey at all, core supplies a default set:sidebar_first,sidebar_second,content,header,primary_menu,secondary_menu,footer,highlighted,helpandbreadcrumb, together with the hiddenpage_topandpage_bottom. The defaultpage.html.twigprints 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_topinblock.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>
Source: Adding regions to a theme
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>
Source: Adding regions to a theme
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
cssorjs(with the categorybase,layout,component,stateorthemefor CSS) that replaces one file with another file, or removes it withfalse -
a string that replaces the whole library with a library of the theme
-
falseto 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/systemforsystem/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
-
logois a path relative to the info file. When the key is missing, core useslogo.svgin 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. -
screenshotis the image on the Appearance page and defaults toscreenshot.pngin the theme root; the documentation recommends 588 by 438 pixels. Core prefixes the path with the theme’s path. -
ckeditor5-stylesheetslists 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 olderckeditor_stylesheetskey 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
Source: Starterkit theme generator
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.
The steps in prose:
-
Discovery: core scans
themes/,core/themes/and the profile directories for*.info.ymlfiles and keeps the ones withtype: theme. -
Parsing: the YAML is parsed and cached with the extension list. A missing
base themeraises an exception; abase theme: falseis removed. The base theme, when present, is added to the dependencies. -
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. -
Alter:
hook_system_info_alter()implementations can change the information of any extension. The System module uses it to hidepage_topandpage_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. -
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, orlogo.svgin 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
-
Go to Appearance (
admin/appearance). -
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.
-
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. -
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
Source: Drush: theme:install
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-stylesheetsand 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-overridecan 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, |
"Missing required key base theme" exception |
The key is absent. Add |
A base theme error such as |
The example was written for Drupal 8 or 9. Classy and Stable were removed in Drupal 10.0; use Starterkit or |
Blocks vanished after editing |
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 |
The toolbar or analytics disappeared |
|
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 |
A colon followed by a space inside an unquoted value ends the key. Quote strings containing a colon, as in
|
Changes to |
The info file and library definitions are cached. Run |
CKEditor does not use the theme’s styling |
|
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.xidentified compatible versions;core_version_requirementarrived in 8.7.7 and became mandatory in Drupal 9.logowas added in 8.6 anddependenciesin 8.9. -
Drupal 9.3:
ckeditor5-stylesheetsadded; 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.3or later. -
Drupal 11.4:
listAllRegions()andlistVisibleRegions()on the Theme object replacesystem_region_list()and theREGIONS_*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: falseor a Starterkit-generated theme as the starting point. -
Material from the Drupal 8 era still shows
core: 8.x,ckeditor_stylesheetsandstylesheets-remove. The first is invalid, the second belongs to CKEditor 4, and the third was replaced bylibraries-override. -
Drupal 10 Development Cookbook (2023) uses
drush theme:enable; it still works, andtheme:installis the primary name of the command withtheme:enableas an alias.