Creating Custom Drupal Templates: Step-by-Step Tutorials

Introduction

I’ve worked on high-traffic sites and seen how targeted template changes reduce front-end weight and improve maintainability. Custom templates in Drupal let you control markup, accessibility, and performance at the theme layer.

This tutorial focuses on Drupal 10 and shows practical steps you can apply immediately: creating a basic node template with Twig, adding and attaching CSS/JS through libraries, and using preprocess hooks to keep logic out of templates. It also includes troubleshooting and security guidance so you can apply these changes safely on production sites.

Introduction to Drupal Templates: What You Need to Know

Understanding Drupal's Template System

Drupal templates determine the HTML structure output for entities such as nodes, blocks, views, and pages. Templates live in the theme's templates/ directory and are rendered by Twig. Knowing the template naming conventions and when to use preprocess hooks makes it easy to keep logic out of templates and maintain performance.

  • Templates control layout and markup for specific render contexts.
  • Twig is the templating engine used in Drupal 8/9/10.
  • Override templates in your active theme's templates/ folder.
  • Prefer preprocess functions to inject variables instead of heavy logic in Twig.

Setting Up Your Drupal Environment for Template Development

Essential Tools and Software

Use a reproducible local environment that matches your production PHP and webserver. Common tooling used by Drupal developers includes:

  • Lando (recommended for reproducible containers) — widely used with Drupal projects.
  • DDEV — another popular Docker-based local environment for PHP applications.
  • MAMP / XAMPP — useful for simple local setups on macOS/Windows.
  • Drush (e.g., Drush 10/11) for command-line site operations.

Verify your PHP version before installing Drupal 10; Drupal 10 requires PHP 8.1 or later. Check PHP on your machine with:

php -v

Download Drupal from the official project site and follow the installer. Use version control (Git) and create a dev branch for theme work to avoid touching production directly.

Choosing a Base Theme

When starting a new theme or customizing templates, choose an appropriate base theme. Drupal core provides common starting points such as Stable, Classy, and the Starterkit approach. Picking a base theme affects available CSS classes, existing templates, and how much you need to override.

  • Stable: minimal markup, useful when you want full control over output.
  • Classy: provides many default utility classes and markup patterns used by core components.
  • Starterkit (theme scaffold): for creating a custom theme that copies core template structure into your theme for safe editing.

Recommendation: if you want lean markup and full control, base on Stable or start with Starterkit and take only the templates you modify. For faster results with consistent class names, Classy can reduce the amount of work.

For core docs and theme starter guidance, see the official Drupal project site: https://www.drupal.org/.

Understanding the Drupal Theming Layer and Template Hierarchy

Exploring Theming Layer Basics

The Drupal theming layer combines render arrays, theme hooks, Twig templates, and asset libraries. The template naming hierarchy enables granular overrides — for example, Drupal will use node--article.html.twig for article nodes before falling back to node.html.twig.

  • Render arrays provide structured data to the theming system.
  • Preprocess functions (hook_preprocess_HOOK) allow variable manipulation before rendering.
  • Theme libraries defined in THEME.libraries.yml manage CSS/JS assets and dependencies.

hook_theme() and Custom Theme Hooks

In addition to file-based Twig templates, modules and themes can register theme hooks using hook_theme(). This allows you to define named renderable elements, expose variables to templates, and point Drupal to template files stored in a module or theme.

Example: register a custom theme hook in a module so Drupal knows where to find the template and which variables to pass.

 [
      'variables' => ['items' => NULL],
      'template' => 'my-custom-item',
      'path' => drupal_get_path('module', 'mymodule') . '/templates',
    ],
  ];
}

After implementing hook_theme(), place my-custom-item.html.twig in the module's templates/ directory. Use render arrays with the #theme key (for example ['#theme' => 'my_custom_item', '#items' => $data]) so Drupal invokes your template.

Advanced Twig Features

For advanced templating needs, you can add custom Twig functions, filters, or extensions. Use them sparingly — push logic to preprocess/PHP when possible, but extensions are useful for reusable presentation helpers (for example, obfuscating emails or returning computed asset versions).

Example: Add a Twig filter to obfuscate email addresses

Implement a small Twig extension in a custom module. This example shows the PHP class and a service registration. Use these only in development with proper reviews for security.

namespace Drupal\mymodule\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class ObfuscateExtension extends AbstractExtension {
  public function getFilters() {
    return [
      new TwigFilter('obfuscate_email', [$this, 'obfuscateEmail']),
    ];
  }

  public function obfuscateEmail($email) {
    return str_replace('@', ' [at] ', $email);
  }
}

Service definition in mymodule.services.yml (module root):

services:
  mymodule.twig.obfuscate_extension:
    class: Drupal\mymodule\Twig\ObfuscateExtension
    tags:
      - { name: twig.extension }

Usage in Twig:

{{ user_mail|obfuscate_email }}

Notes:

  • Keep extensions focused and small; heavy logic belongs in PHP or services used by preprocess functions.
  • Always document new Twig functions/filters and add unit tests where possible.

Template Rendering Flow (Diagram)

Quick mapping: the browser request is routed → controller/entity builds render arrays → theme layer runs preprocess hooks and selects templates → Twig renders HTML and assets are attached. The diagram below visualizes these stages and their relationships.

Drupal Template Rendering Flow Browser request → Routing/Controller → Theme Layer (preprocess, Twig, assets) → Rendered HTML response Browser HTTP Request Routing / Controller Resolve entity / render array Theme Layer Preprocess → Twig → Assets Rendered HTML Response to Browser
Figure 1: Simplified Drupal template rendering flow — request, routing/controller, theme layer (preprocess & Twig), final HTML response.

Creating Your First Custom Template: A Hands-On Tutorial

Setting Up Your Custom Template

Locate your active theme directory (example: themes/custom/mytheme). Create or open templates/ inside your theme. When overriding a node template for the "article" content type, the filename should be node--article.html.twig.

Clear caches after creating or editing templates so Drupal picks up file changes:

drush cr

Enable Twig debugging during development to see which template suggestions are available. In your sites/development.services.yml (or sites/default/services.yml for local dev), set the twig config as follows (only enable on local/dev):

parameters:
  twig.config:
    debug: true
    auto_reload: true
    cache: false

With twig debug enabled, view the page source in the browser to find template suggestions and confirm which template Drupal is using.

Twig Debugging (Dev Only) — WARNING

Important: enable Twig debugging only in local or development environments. Twig debug exposes template paths, suggestions, and some variable structures in page source — which can leak implementation details and sensitive paths. Additionally, leaving Twig debug and cache disabled harms performance.

For production, ensure twig debug is disabled and caching is enabled. Example production configuration (set in your production services.yml or environment-specific configuration):

parameters:
  twig.config:
    debug: false
    auto_reload: false
    cache: true

After changing these settings, always clear caches (drush cr) and confirm debug output is no longer present in page source. Treat enabling debug as a temporary developer tool, not a runtime mode for live sites.

Step-by-Step Example: node--article.html.twig

Below is a minimal starting template for an article node. It demonstrates printing the title and rendered content using the standard Twig variables. Place this file at themes/custom/mytheme/templates/node--article.html.twig.

{# node--article.html.twig #}
<article{{ attributes.addClass('node', 'node--article') }}>
  {% if label %}
    <h1{{ title_attributes.addClass('node__title') }}>{{ label }}</h1>
  {% endif %}

  <div class="node__meta">
    {{ author_picture }}
    <span class="node__submitted">{{ author_name }} — {{ created }}</span>
  </div>

  <div class="node__content">
    {{ content|without('comments', 'links') }}
  </div>

  {% if content.comments %}
    <div class="node__comments">{{ content.comments }}</div>
  {% endif %}
</article>

Notes:

  • Use |without() to exclude regions from content when rendering fields selectively.
  • Twig auto-escapes output by default; avoid using the raw filter unless you have sanitized HTML.

If you need a custom field displayed differently, use a preprocess function to prepare the variable (see the Best Practices section for a preprocess example).

Enhancing Templates with CSS and JavaScript for Better UX

Adding Styles and Scripts via Theme Libraries

Define assets in your theme's mytheme.libraries.yml and attach them globally in mytheme.info.yml or attach per-template via #attached in preprocess. Example files below show a small CSS and JS snippet and how to register them.

Create a file themes/custom/mytheme/css/custom.css with the following minimal styles:

/* custom.css */
.custom-article {
  background-color: #f0f0f0;
  padding: 18px;
  border-radius: 6px;
}
.node__title {
  color: #333333;
  font-size: 26px;
}

Create a file themes/custom/mytheme/js/custom.js with this simple script:

// custom.js
(function (Drupal, drupalSettings) {
  'use strict';
  console.log('Custom script loaded!');
})(Drupal, drupalSettings);

Example mytheme.libraries.yml:

global-styling:
  css:
    theme:
      css/custom.css: {}
  js:
    js/custom.js: {}
  dependencies:
    - core/drupal
    - core/jquery

Attach the library globally in mytheme.info.yml so it loads on every page:

libraries:
  - mytheme/global-styling

Alternatively, attach the library only for article nodes in a preprocess function:

function mytheme_preprocess_node(array &$variables) {
  if ($variables['node']->getType() === 'article') {
    $variables['#attached']['library'][] = 'mytheme/global-styling';
  }
}

Security and performance tips for assets:

  • Avoid inline scripts/styles when possible to make a stricter Content-Security-Policy (CSP) feasible.
  • Minify and aggregate CSS/JS for production (Drupal aggregation settings or build tools such as PostCSS).
  • Use the library dependency mechanism to ensure jQuery or Drupal behaviors are loaded in the correct order.

Performance Optimization Beyond Basic Aggregation

Beyond Drupal’s built-in aggregation, focus on delivering only what’s needed per page and deferring non-critical work.

  • Lazy-load images and media: Use the loading="lazy" attribute and Drupal responsive image styles where appropriate.
  • Critical CSS: Extract critical CSS for above-the-fold content and defer non-critical styles. Use build tools (PostCSS/CSSNano) to extract and inline only essential rules for the initial render.
  • Use BigPipe for progressive delivery: BigPipe (core module) can stream cacheable content first and replace placeholders with personalized or heavy content later.
  • Component-level caching: Add cache metadata to render arrays (#cache) with appropriate contexts and tags to maximize reuse while ensuring invalidation works correctly.
  • Image optimization: Produce multiple image derivatives and use responsive image styles; integrate an image-optimization pipeline (CI or on-upload) that creates WebP or AVIF variants for modern browsers.

Example: Add simple cache metadata on a render array in PHP so the template output is cached properly:

$build = [
  '#theme' => 'my_custom_item',
  '#items' => $items,
  '#cache' => [
    'contexts' => ['user.roles:authenticated'],
    'tags' => ['node_list'],
    'max-age' => 3600,
  ],
];

Real-World Debugging Techniques

Inspecting complex render arrays and template variables is common during theming. Use the right tools and guard them to development environments only.

  • Devel & Kint: The Devel module provides kint() and the Twig kint() function to inspect variables in templates. Example in Twig: {{ kint(content.field_my_complex) }}. Only enable Devel/Kint on non-production sites.
  • Twig dump(): Use {{ dump(variable) }} in templates to print a structured representation. Wrap with environment checks to avoid exposing data in production.
  • Server-side debugging: Use \Kint::dump($var); or simple error_log(print_r($var, TRUE)); in preprocess to inspect variables during request processing.
  • Browser devtools & network: Inspect CSS, markup, and asset hashes. Verify library attachments by checking page HTML and network requests for the CSS/JS files.

Example pattern to keep debug code local-only in Twig:

{% if app.environment == 'dev' %}
  {{ kint(content) }}
{% endif %}

Note: Ensure any debugging statements are removed or disabled before deploying to production to avoid information disclosure and performance overhead.

Theme Inheritance / Sub-theming

Sub-themes allow you to extend a base theme and override or add templates and assets without copying the entire base theme. Use sub-themes when you want to inherit a stable set of templates and styles while customizing a subset.

Minimal mysubtheme.info.yml example that inherits from Stable or Classy:

name: My Subtheme
type: theme
base theme: classy
description: "A subtheme that customizes a few templates and styles."
core_version_requirement: ^10
libraries:
  - mysubtheme/global-styling

Place overrides in themes/custom/mysubtheme/templates/ and define only the libraries and templates you change. This keeps upgrades simpler since the base theme can receive updates independently.

When to use sub-themes:

  • You need consistent base behavior across multiple projects but with small customizations.
  • You want to keep core or contributed theme updates separate from project-specific changes.

Best Practices and Troubleshooting for Custom Drupal Templates

Implementing Best Practices

Follow these concrete guidelines to keep theme code maintainable and secure:

  • Name templates using Drupal conventions (e.g., node--content-type.html.twig).
  • Keep heavy logic in module PHP or preprocess functions; templates should only handle presentation.
  • Use Twig’s escaping and the without() filter to avoid printing undesired fields.
  • Document theme changes and include a changelog in the theme directory for team handoffs.

Example preprocess function to add a custom class and a derived variable to the template (fixed and using recommended APIs):

function mytheme_preprocess_node(array &$variables) {
    $node = $variables['node'];
    if ($node->getType() === 'article') {
        // Add a CSS class used in the Twig template
        if (isset($variables['attributes']) && method_exists($variables['attributes'], 'addClass')) {
            $variables['attributes']->addClass('custom-article');
        }

        // Add a lightweight derived variable for display
        $variables['display_date'] = \Drupal::service('date.formatter')->format($node->getCreatedTime(), 'long');
    }
}

Troubleshooting Common Issues

When changes don't appear, verify these common causes and fixes:

  • Cache: Run drush cr or clear caches from the admin UI.
  • Twig debug: Enable twig debug to view template suggestions and confirm which template renders the page.
  • Wrong file path/name: Confirm filename and that it's in themes/custom/mytheme/templates/.
  • Asset not loading: Check that the library is declared in mytheme.libraries.yml and attached via info.yml or preprocess.
  • Permission issues: Ensure file permissions allow the web server user to read theme files.

Use browser devtools to inspect markup and network requests, and check Drupal logs (Admin > Reports > Recent log messages) for PHP notices.

Key Takeaways

  • Use Twig and template naming conventions to target specific output types like node--article.html.twig.
  • Define styles and scripts in THEME.libraries.yml and attach them via THEME.info.yml or preprocess for modular loading.
  • Keep logic in preprocess functions to keep templates clean and maintainable.
  • Enable twig debug and use Drush for quick cache rebuilds during theme development, but never leave debug enabled in production.

Frequently Asked Questions

What is the best way to start creating custom templates in Drupal?
Start with Twig fundamentals, confirm twig debugging is enabled for template suggestions, and create a minimal override (for example node--article.html.twig) in your theme. Use local environments like Lando or DDEV for reliable testing.
How can I optimize my Drupal templates for performance?
Minimize DOM depth, combine and aggregate CSS/JS for production, leverage Drupal’s cache metadata on render arrays, and move complex computation to background processes or preprocess hooks so rendering stays fast.

Conclusion

Custom Drupal templates let you control markup, accessibility, and asset loading. The recommended workflow is: create a minimal Twig override, use preprocess hooks for variables and attachments, define assets in .libraries.yml, and validate changes with twig debug and Drush. This approach keeps templates maintainable and production-safe.

For more theme documentation and community resources, refer to the official Drupal project site: https://www.drupal.org/.

About the Author

Marcus Chen

Marcus Chen is a Web Performance Engineer & Frontend Architect with 12 years of experience specializing in performance optimization, CSS architecture, and accessibility (WCAG 2.1). He focuses on practical, production-ready solutions and has worked on a range of large-scale projects.


Published: Jul 22, 2025 | Updated: Jan 07, 2026