Skip to content

TutorialFor theme developersDrupal 11

Build your first theme 1: start from the starterkit

This is part 1 of a six-part tutorial that builds a complete Drupal 11 theme from the command line up: templates, CSS, a single-directory component, JavaScript, and theme settings. In this part you generate the theme and make Drupal render every page through it.

A standalone theme named First theme (machine name first_theme), generated from Drupal core’s starterkit. By the end of this part:

  • the theme exists in web/themes/custom/first_theme with markup you own outright,
  • its first_theme.info.yml declares your own regions, libraries, and logo,
  • it is installed and set as the default theme, so every visitor-facing page renders through it.

Later parts add Twig template overrides (part 2), CSS and breakpoints (part 3), a card component (part 4), a JavaScript behavior (part 5), and theme settings plus responsive images (part 6).

What the theme tutorial builds

Theming means owning files in your own theme, generated by the starterkit, not editing core templates. Each tutorial part adds its piece to the first_theme directory: part 1 generates the info file, part 2 adds Twig template overrides, part 3 asset libraries and CSS, part 4 a single-directory component, part 5 a JavaScript behavior, part 6 theme settings and responsive image config.

Commands below run from the project root (the directory with composer.json), with the Drupal docroot in web/. They run inside the DDEV container: ddev drush for Drush, and ddev exec for anything else.

Drupal core ships a console command, GenerateTheme, that creates a new theme by copying core’s hidden starterkit_theme and renaming everything in it to your machine name:

Terminal window
ddev exec vendor/bin/dr generate-theme first_theme \
--name "First theme" --path themes/custom

The command resolves --path against the Drupal root, so this creates web/themes/custom/first_theme no matter where you run it from. It prints:

Theme generated successfully to themes/custom/first_theme

Added in 11.4 vendor/bin/dr is core’s CLI entry point. On Drupal 11.3 and earlier, run the same command through ddev exec php web/core/scripts/drupal generate-theme ... instead; from 11.4 on that script is a deprecated alias for dr and is removed in Drupal 13 (see the change record).

Why copy instead of inherit? The generated first_theme.info.yml contains 'base theme': false, which makes this a standalone theme: Drupal never falls back to another theme’s templates or CSS at runtime. The copy is stamped with a generator key recording which starterkit version it was forked from, so you can diff against later core releases on your own schedule. The alternative, subtheming a live base theme, means core or contrib markup changes land in your site the moment you update, whether you reviewed them or not.

Step 2: Install it and set it as the default

Section titled “Step 2: Install it and set it as the default”

Drupal discovered the new theme as soon as the directory appeared; now install it and make it the default.

  1. Go to Administer → Appearance/admin/appearance.
  2. Find First theme under Uninstalled themes and select Install and set as default.

The .info.yml file is the theme’s manifest: Drupal reads it to learn what the theme is called, which assets to load on every page, and which regions it offers to the block system. Replace the generated first_theme.info.yml with the version this tutorial builds toward:

web/themes/custom/first_theme/first_theme.info.yml
name: First theme
type: theme
description: 'A standalone (starterkit-style) theme built in the "Build your first theme" tutorial.'
package: Documentation examples
core_version_requirement: ^11
# Standalone themes own all of their markup and CSS. Generate one with:
# php core/scripts/drupal generate-theme first_theme
'base theme': false
logo: logo.svg
libraries:
# Attached on every page. Defined in first_theme.libraries.yml.
- first_theme/global
- core/normalize
regions:
header: Header
primary_menu: 'Primary menu'
breadcrumb: Breadcrumb
highlighted: Highlighted
content: Content
sidebar: Sidebar
footer: Footer

Walk through the keys that do the work:

  • type: theme and core_version_requirement: ^11 tell the extension system what this is and where it runs. They are required.
  • 'base theme': false declares the theme standalone. The key is quoted because it contains a space, and it is required for every theme: Drupal throws a “Missing required key” exception for a theme that declares neither a base theme nor false.
  • logo: logo.svg points at the logo file, relative to the theme directory. logo.svg is also the path Drupal assumes when the key is absent, so this line is documentation as much as configuration; the generator copied a placeholder logo.svg for you.
  • libraries: lists asset libraries attached to every page the theme renders. core/normalize is core’s copy of normalize.css. first_theme/global does not exist yet: you define it in part 3. Drupal skips library references it cannot resolve rather than failing, so the site keeps working, but with almost no CSS. That bare look is the honest starting point for the styling work ahead.
  • regions: declares the named areas the block system can place blocks into. Declaring this key replaces the default region list (sidebar_first, secondary_menu, help, and friends) entirely. Keep a content region: that is where Drupal places the main page content block.

If you plan to diff against future starterkit releases, also keep the version and generator lines the command wrote; they are metadata and change nothing at runtime.

Rebuild caches so Drupal re-reads the file:

Terminal window
ddev drush cache:rebuild

Step 4: Print your regions in the page template

Section titled “Step 4: Print your regions in the page template”

A region only appears on the page if the page template prints it. The generated templates/layout/page.html.twig still prints the starterkit’s region set (page.sidebar_first, page.sidebar_second, page.secondary_menu, page.help), and those no longer exist. Open it and make it match your regions: block: delete the markup for regions you removed, and print the one you added:

{% if page.sidebar %}
<aside class="layout-sidebar">{{ page.sidebar }}</aside>
{% endif %}

The page.* variables are render arrays, one per declared region; printing one renders every block placed in it. The info file and the page template are two halves of one contract, and Drupal does not check them against each other: a region declared but never printed is invisible, and a page.* variable printed but never declared is always empty.

“Theme could not be generated because the destination directory exists already”

Section titled ““Theme could not be generated because the destination directory exists already””

generate-theme refuses to overwrite an existing directory. Delete the previous attempt (or pick another machine name) and run the command again.

“The source theme starterkit_theme has a development version number”

Section titled ““The source theme starterkit_theme has a development version number””

You are running against a development checkout of core rather than a tagged release. The command wants to record the exact starterkit commit in the generator key and needs git for that. Install git, or generate from a tagged core release.

The theme does not appear on the Appearance page

Section titled “The theme does not appear on the Appearance page”

Drupal scans themes/ inside the docroot. Check that the theme landed in web/themes/custom/first_theme (not a top-level themes/ next to composer.json) and that the info file is named exactly first_theme.info.yml. Then run ddev drush cache:rebuild.

Block layout warns that blocks were assigned to an invalid region

Section titled “Block layout warns that blocks were assigned to an invalid region”

Expected after step 3: declaring regions: replaced the default region list, and the Block module disables any block sitting in a region that no longer exists. The warning names each block; reassign the ones you want to keep to one of your seven regions.

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.