Skip to content

TutorialFor theme developersDrupal 11

Build your first theme 6: settings, responsive images, and shipping

In the final part of the theme tutorial you give site builders a setting, turn part 3’s breakpoints into responsive images, and take stock of what ships.

  • A Show drop shadows on cards checkbox on First theme’s settings page, stored in configuration, read through the modern theme-settings API, and wired to the teaser cards.
  • A responsive image style built on the first_theme breakpoint group from part 3, applied to article images.
  • A finished theme, ready to commit and deploy.
  • First theme as part 5 left it.
  • At least one article with an image, for step 3.

Every installed theme gets a settings page under Administer → Appearance with core’s shared options (logo, favicon). A theme extends that form by implementing hook_form_system_theme_settings_alter() in a theme-settings.php file in its root; core includes that file (and the .theme file) right before building the form. Create theme-settings.php, starting with the opening <?php tag and a @file docblock, then add:

web/themes/custom/first_theme/theme-settings.php
<?php
/**
* @file
* Theme settings form for the First theme.
*/
declare(strict_types=1);
use Drupal\Core\Extension\ThemeSettingsProvider;
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_system_theme_settings_alter().
*
* Adds theme-specific options to Appearance > Settings > First theme.
* Read the value in preprocess hooks with the ThemeSettingsProvider
* service, then pass it to your templates as a variable.
*/
function first_theme_form_system_theme_settings_alter(array &$form, FormStateInterface $form_state): void {
$form['first_theme_settings'] = [
'#type' => 'details',
'#title' => t('First theme settings'),
'#open' => TRUE,
];
$form['first_theme_settings']['show_card_shadows'] = [
'#type' => 'checkbox',
'#title' => t('Show drop shadows on cards'),
'#default_value' => \Drupal::service(ThemeSettingsProvider::class)->getSetting('show_card_shadows') ?? TRUE,
];
}

This is ordinary Form API: a collapsible details wrapper and a checkbox. Two lines deserve a closer look:

  • The element key, show_card_shadows, becomes the storage key. On submit, core saves every custom form value into the theme’s configuration object, first_theme.settings, so the setting deploys, exports, and imports like any other configuration.
  • #default_value reads the current value through the ThemeSettingsProvider service, with ?? TRUE supplying the default while nothing is saved yet. The static \Drupal::service() call is the correct form here, because this hook is a procedural function where constructor injection does not exist.

Step 2: Read the setting the non-deprecated way

Section titled “Step 2: Read the setting the non-deprecated way”

For years the way to read a theme setting was the global function theme_get_setting(). It still exists, and it is this track’s biggest landmine:

The replacement is the service you already used in step 1. Its getSetting() method takes the setting name and an optional theme name, defaulting to the active theme, and returns the value layered from the global system.theme.global defaults and the theme’s own THEME.settings configuration.

To act on the setting, read it in a preprocess function. Open the first_theme.theme file the generator created, and add the use line below its @file docblock and the function below it:

web/themes/custom/first_theme/first_theme.theme
use Drupal\Core\Extension\ThemeSettingsProvider;
/**
* Implements hook_preprocess_HOOK() for node templates.
*
* Adds the card--shadow class to teasers when the "Show drop shadows on
* cards" theme setting is on. node--teaser.html.twig passes the node's
* attributes to the card, so the class lands on the card wrapper.
*/
function first_theme_preprocess_node(array &$variables): void {
$show_shadows = \Drupal::service(ThemeSettingsProvider::class)->getSetting('show_card_shadows') ?? TRUE;
if ($show_shadows && $variables['view_mode'] === 'teaser') {
$variables['attributes']['class'][] = 'card--shadow';
}
}

The function adds a card--shadow class to each teaser’s attributes, and node--teaser.html.twig from part 4 passes those attributes to the card, so the class lands on the card’s article element. Theme hooks like hook_preprocess_HOOK() stay procedural in theme files; that rule from part 2 applies to preprocessing too.

Then give the class a style, at the end of components/card/card.css:

web/themes/custom/first_theme/components/card/card.css
/* Added by first_theme_preprocess_node() when the theme setting is on. */
.card--shadow {
box-shadow: 0 2px 8px rgb(0 0 0 / 0.12);
}

Saving theme settings invalidates rendered output, so the change shows on the next page load.

Step 3: Turn breakpoints into responsive images

Section titled “Step 3: Turn breakpoints into responsive images”

Part 3’s first_theme.breakpoints.yml declared three screen widths and 1x/2x multipliers. The Responsive Image module reads them and renders a picture element that lets the browser download only the size it needs.

  1. Install the module: ddev drush pm:install responsive_image.
  2. Go to Administer → Configuration → Media → Responsive image styles/admin/config/media/responsive-image-style and select Add responsive image style. (On the standard profile, installing the module pulls in two generic styles the profile ships as optional configuration, Narrow and Wide; you are building one against your own theme’s breakpoints instead.)
  3. Enter Article hero as the label and choose First theme under Breakpoint group. The form now lists each breakpoint and multiplier from your YAML file: first_theme.mobile, first_theme.tablet, and first_theme.desktop, twice each.
  4. For each breakpoint you want to serve, pick Select a single image style. and choose an image style of an appropriate width (core ships Large, Medium, and Wide; tighter control comes from defining your own image styles, which the images and media guide covers).
  5. Choose a Fallback image style for browsers that ignore picture, and save.
  6. Apply it: go to Administer → Structure → Content types → Article → Manage display, change the Image field’s format to Responsive image, select the gear, pick Article hero as the responsive image style, Update, then Save.

The division of labor: the theme owns the breakpoints (they describe its design), while the responsive image style and the display configuration own which image sizes map onto them.

Look at what web/themes/custom/first_theme contains now (plus whatever you kept from the starterkit copy):

  • first_theme.info.yml
  • first_theme.libraries.yml
  • first_theme.breakpoints.yml
  • first_theme.theme
  • theme-settings.php
  • logo.svg
  • screenshot.png
  • Directorycomponents/
    • Directorycard/
      • card.component.yml
      • card.twig
      • card.css
  • Directorycss/
    • base.css
  • Directoryjs/
    • first-theme.js
  • Directorytemplates/
    • node.html.twig
    • node—teaser.html.twig

Shipping it is unglamorous on purpose:

  • Commit the directory. A custom theme is code; it belongs in the project’s repository like any module.
  • Export configuration. The choices that live outside the theme directory (default theme, first_theme.settings, the responsive image style, the display settings) are configuration: ddev drush config:export captures them so deployment reproduces them. Configuration management explains the workflow, and the deployment guide places it in the release process.
  • Check the Appearance listing. The screenshot.png the generator copied is what shows on /admin/appearance; replace it with a screenshot of your actual theme before anyone else has to pick it from a list.

The settings form does not show my checkbox

Section titled “The settings form does not show my checkbox”

The file must be named theme-settings.php, in the theme root, and the function must be named exactly first_theme_form_system_theme_settings_alter. Rebuild caches after creating the file.

Deprecation notices about theme_get_setting() in the log

Section titled “Deprecation notices about theme_get_setting() in the log”

Some code, often copied from older tutorials, still calls the deprecated function. Replace each call with \Drupal::service(ThemeSettingsProvider::class)->getSetting(...) as in step 2; the signature maps one to one.

“First theme” is missing from the Breakpoint group select

Section titled ““First theme” is missing from the Breakpoint group select”

The breakpoint group only exists if first_theme.breakpoints.yml was discovered: rebuild caches and recheck the part 3 checkpoint (getBreakpointsByGroup('first_theme')). The group appears under the theme’s human-readable name.

The image still renders as a plain img element

Section titled “The image still renders as a plain img element”

The field format is still a regular image style. Recheck the last item of step 3: the format must be Responsive image with your responsive image style selected, on the view mode you are actually looking at (the front page teaser and the full node use different displays).

Was this page helpful?

Drupal is a registered trademark of Dries Buytaert.