How To Create A Custom Drupal Theme Settings Form
A custom theme settings form gives site administrators a controlled way to change presentation options without editing Twig files, CSS, or configuration files by hand. In Drupal, these options can include a brand colour, a footer message, a layout choice, a navigation behaviour, or a switch that enables an optional banner.
The approach works well for Drupal 10 and Drupal 11 themes because it uses the Form API and the existing theme settings screen. Editors can manage the values from Appearance, while the theme reads them through Drupal’s configuration system. This keeps implementation practical for Australian organisations, from a Melbourne community group to a Perth retailer managing content across different time zones.
Understand How Theme Settings Work
Drupal provides a theme-specific form alteration hook named hook_form_system_theme_settings_alter(). The hook allows a theme to add fields to the configuration form shown at /admin/appearance/settings/THEME_MACHINE_NAME. Replace the placeholder with the machine name of the active theme.
The implementation normally lives in the theme’s .theme file. For a theme named harbour, the file would be:
themes/custom/harbour/harbour.theme
The function name must use the theme machine name:
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_system_theme_settings_alter().
*/
function harbour_form_system_theme_settings_alter(
array &$form,
FormStateInterface $form_state
) {
// Custom fields will be added here.
}
Drupal’s existing theme settings form supplies the submit handling, so basic fields added to the form are saved with the theme’s settings configuration. This is preferable to creating a separate administration form when the values are strictly concerned with visual presentation.
A theme setting should represent a genuine configuration choice rather than an arbitrary content field. A footer disclaimer, for example, may belong in a block or a content entity if editors need rich text, revision history, and publishing controls. A short plain-text footer label or a layout selection is a better candidate for theme configuration.
Prepare The Theme And Configuration Files
Before adding fields, confirm that the custom theme has a valid .info.yml file and is installed as a development theme. A minimal definition might look like this:
name: Harbour
type: theme
base theme: stable9
core_version_requirement: ^10 || ^11
libraries:
- harbour/global
The .theme file is loaded by Drupal when the theme is active. Clear caches after creating the file or changing the function name:
drush cr
A custom theme can provide initial values in a configuration file under config/install. For the harbour theme, use:
themes/custom/harbour/config/install/harbour.settings.yml
Example contents:
brand_colour: '#005f73'
show_alert: false
footer_label: 'Proudly serving Australia'
layout_width: 'wide'
These values are installed when the theme is installed. They are not automatically reapplied whenever the theme is enabled, so changes to defaults should be handled through configuration management or an update process on an existing site.
A schema file documents the data types and helps Drupal validate configuration:
themes/custom/harbour/config/schema/harbour.schema.yml
harbour.settings:
type: theme_settings
label: 'Harbour theme settings'
mapping:
brand_colour:
type: string
label: 'Brand colour'
show_alert:
type: boolean
label: 'Show alert'
footer_label:
type: string
label: 'Footer label'
layout_width:
type: string
label: 'Layout width'
The schema becomes increasingly important when configuration is exported, imported, inspected, or checked by development tools. It also makes the purpose of each custom value clear to another developer taking over a Drupal project.
Add Fields With Drupal Form API
The form alteration function can now expose useful controls. This example adds a colour field, a checkbox, a text field, and a select list:
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_form_system_theme_settings_alter().
*/
function harbour_form_system_theme_settings_alter(
array &$form,
FormStateInterface $form_state
) {
$form['harbour_options'] = [
'#type' => 'details',
'#title' => t('Harbour theme options'),
'#open' => TRUE,
];
$form['harbour_options']['brand_colour'] = [
'#type' => 'color',
'#title' => t('Brand colour'),
'#default_value' => theme_get_setting('brand_colour'),
'#description' => t('Choose the primary colour used by the theme.'),
];
$form['harbour_options']['show_alert'] = [
'#type' => 'checkbox',
'#title' => t('Display the alert banner'),
'#default_value' => theme_get_setting('show_alert'),
];
$form['harbour_options']['footer_label'] = [
'#type' => 'textfield',
'#title' => t('Footer label'),
'#default_value' => theme_get_setting('footer_label'),
'#maxlength' => 120,
];
$form['harbour_options']['layout_width'] = [
'#type' => 'select',
'#title' => t('Content width'),
'#options' => [
'standard' => t('Standard'),
'wide' => t('Wide'),
],
'#default_value' => theme_get_setting('layout_width'),
];
}
theme_get_setting() retrieves the current value for the active theme and is suitable for setting the default value shown in the form. The field names become keys in the theme’s settings configuration. Use stable, descriptive names because templates, preprocess functions, and CSS generation may depend on them later.
For a site serving customers in Sydney and Melbourne, a setting such as show_alert could control a short delivery notice during a public holiday period. A Perth-based organisation might use the same pattern for a regional campaign, but the time-sensitive message itself should generally remain content-managed rather than hard-coded into the theme.
Use t() for labels and descriptions so the administration interface can be translated. Avoid placing HTML inside labels unless there is a strong accessibility reason and the output is carefully handled.
Store And Validate Submitted Values
The standard theme settings form will save basic values, but validation is still important. A colour field supplied by Drupal has an appropriate input type, yet server-side validation should remain the final authority. Administrators, automated requests, or configuration imports can provide values that did not originate from the expected browser control.
Validation can be added to the form element:
$form['harbour_options']['brand_colour']['#element_validate'][] =
'harbour_validate_brand_colour';
Then define the validator in the .theme file:
/**
* Validates the brand colour.
*/
function harbour_validate_brand_colour(
array &$element,
FormStateInterface $form_state,
array &$complete_form
) {
$value = $form_state->getValue($element['#parents']);
if (!preg_match('/^#[0-9a-fA-F]{6}$/', $value)) {
$form_state->setError(
$element,
t('Enter a valid six-digit hexadecimal colour.')
);
}
}
For a text setting, set a suitable maximum length and decide whether markup should ever be allowed. If the value is printed with Twig’s normal escaping, plain text remains safe. Do not mark administrator-entered text as safe merely to make HTML appear in the output.
When a setting needs a more involved process, such as conditional fields or a custom submit action, add a submit handler carefully. The existing theme settings form is responsible for persisting the standard values, so custom submit logic should complement it rather than replace it. If the requirement has grown into permissions, entity references, media selection, or multi-step workflows, a dedicated configuration form may be more suitable.
Read Settings In Twig And Preprocess Functions
Once a value is saved, a Twig template can retrieve it with the theme_settings variable in many theme contexts:
<footer class="site-footer">
{% if theme_settings.footer_label %}
<p>{{ theme_settings.footer_label }}</p>
{% endif %}
</footer>
The exact available variables depend on the template and Drupal version, so a preprocess function is often clearer for reusable data. For example, add a class based on the selected layout in harbour.theme:
/**
* Implements hook_preprocess_html().
*/
function harbour_preprocess_html(array &$variables) {
$layout = theme_get_setting('layout_width') ?: 'standard';
$variables['attributes']['class'][] = 'layout-' . $layout;
}
The value used in a class name must come from a controlled list. Never pass arbitrary administrator input directly into a class, an ID, a URL, or an inline style without validation and appropriate escaping. A select list with known values is safer than a free-text field for presentation states.
For the colour setting, a CSS custom property can be added through a preprocess function, but inline styles require extra care. A safer pattern is to map approved values to predefined classes:
function harbour_preprocess_html(array &$variables) {
$colour = theme_get_setting('brand_colour') ?: '#005f73';
$allowed_colours = [
'#005f73' => 'theme-colour-ocean',
'#d1495b' => 'theme-colour-coral',
];
$variables['attributes']['class'][] =
$allowed_colours[$colour] ?? 'theme-colour-ocean';
}
The CSS then controls the actual presentation:
.theme-colour-ocean {
--brand-colour: #005f73;
}
.theme-colour-coral {
--brand-colour: #d1495b;
}
This approach supports a controlled design system and makes contrast testing easier. A colour that looks acceptable on a designer’s monitor may fail WCAG contrast requirements, particularly on small text or mobile screens used by commuters in Brisbane or Adelaide.
Test, Export, And Maintain The Form
After adding the form, clear Drupal caches and visit the theme settings page. Check that the fields appear under the intended theme, that initial values are sensible, and that the settings remain after saving and logging in again. Test both valid and invalid input, including an empty text value, an unexpected colour string, and a configuration import.
Configuration management should be part of the workflow for a team. Export the setting after making a deliberate change:
drush cex
The exported configuration can then be committed to version control and imported into another environment:
drush cim
drush cr
Be aware that theme settings can be environment-specific. A staging site may display a test campaign banner while production uses a customer-facing message. Keep environment-dependent values out of a shared deployment where possible, or document the intended override process.
Use Australian spelling in labels where it matches the site’s editorial style, such as “colour” rather than “color”. This small detail helps administration screens feel consistent for local teams and agencies. For an Australian ecommerce site, a theme setting might control whether a compact GST-inclusive pricing notice is visible, but pricing logic should remain in the commerce or content layer.
Practical Recommendations For A Reliable Theme Form
A settings form should be small, predictable, and easy for an editor to understand. Group related fields into details elements, provide useful descriptions, and choose defaults that produce a complete page immediately after the theme is installed.
Keep the implementation compatible with Drupal coding standards and review it whenever the theme is upgraded. The following practices reduce maintenance problems:
- Use the theme machine name consistently in the hook, configuration file, and schema.
- Define installation defaults and document each setting in the configuration schema.
- Prefer select lists and checkboxes when the available values are limited.
- Validate values on the server before using them in markup, classes, styles, or URLs.
- Use
theme_get_setting()in preprocess functions and templates rather than hard-coding configuration. - Clear caches after code changes and test configuration export and import.
- Keep content, campaign copy, and complex editorial workflows outside the theme settings form.
A carefully designed custom Drupal theme settings form gives editors useful control without turning the theme into a second content management system. It also creates a clear boundary between presentation configuration, site content, and application behaviour, which makes ongoing administration much easier.