Drupalwoo ☉

How to Create a Custom Drupal Theme from Scratch

A custom Drupal theme controls how content is presented without changing the site’s underlying editorial data. It defines page structure, typography, colours, responsive behaviour, reusable components, and the relationship between Drupal’s render arrays and browser markup. Building one from scratch is a useful way to create a focused design system instead of adapting a general-purpose theme with many unused features.

This guide uses modern Drupal practices suitable for Drupal 10 and Drupal 11. The examples assume a Composer-managed installation, a theme stored in web/themes/custom, and a development workflow that includes local configuration, version control, browser testing, and cache rebuilding. The same principles apply whether the finished site serves a small business in Adelaide, a community organisation in Brisbane, or a larger publisher in Sydney or Melbourne.

Prepare The Theme Architecture

Start by deciding what the theme is responsible for. A theme should handle presentation, layout, templates, front-end assets, and visual states. Content types, fields, editorial workflows, permissions, and business rules belong in configuration or custom modules. Keeping these responsibilities separate makes the design easier to replace and prevents important site behaviour from disappearing when the theme changes.

Create a directory for the project:

web/
└── themes/
    └── custom/
        └── harbour/
            ├── harbour.info.yml
            ├── harbour.libraries.yml
            ├── harbour.theme
            ├── css/
            ├── js/
            ├── templates/
            └── screenshot.png

The machine name must use lowercase letters, numbers, and underscores where appropriate. The human-readable name can be different. A theme named harbour could be branded as “Harbour Digital”. Avoid spaces and punctuation in the machine name because Drupal uses it in library definitions, Twig functions, and theme hooks.

Drupal’s Starterkit theme generator is often the safest starting point for production work because it creates the expected files and metadata for the installed core version. A generated theme is still fully customisable. If you want to learn the underlying structure or maintain a very small theme, creating the files manually is also practical.

Register Metadata And Front-End Assets

The required .info.yml file tells Drupal that the directory contains a theme. A minimal version can look like this:

name: Harbour
type: theme
base theme: false
core_version_requirement: ^10 || ^11
description: 'A custom responsive theme for a Drupal website.'

libraries:
  - harbour/global

regions:
  header: Header
  primary_menu: Primary menu
  highlighted: Highlighted
  content: Content
  sidebar: Sidebar
  footer: Footer

base theme: false creates a stand-alone theme. A base theme such as Claro or Olivero can provide useful defaults, but you should understand which templates and styles you inherit before overriding them. For a bespoke public-facing site, a clean theme often makes CSS and markup easier to audit.

Declare CSS and JavaScript in harbour.libraries.yml rather than adding files directly inside templates:

global:
  css:
    theme:
      css/base.css: {}
      css/layout.css: {}
      css/components.css: {}
  js:
    js/harbour.js: {}
  dependencies:
    - core/drupal
    - core/once

Attach the library globally through the .info.yml file, or attach a smaller library to a particular component or template. Drupal aggregates and caches these assets, so clear caches after changing library definitions. Avoid placing third-party scripts in Twig unless there is a specific reason; library definitions provide clearer dependency management and more reliable loading behaviour.

CSS custom properties can establish a small design system:

:root {
  --colour-ink: #202124;
  --colour-brand: #075985;
  --colour-surface: #f5f7f8;
  --space-unit: 0.5rem;
  --content-width: 72rem;
}

.container {
  width: min(100% - 2rem, var(--content-width));
  margin-inline: auto;
}

Use relative units, flexible grids, and sufficiently large touch targets. This matters for Australian users on varied connections and devices, including commuters using mobile networks in Melbourne, regional users on NBN services, and visitors accessing a site from older tablets.

Build Twig Templates And Regions

Drupal renders pages through Twig templates. The page.html.twig file is a useful first customisation because it controls the broad document layout:

<header class="site-header">
  <div class="container">
    {{ page.header }}
  </div>
  <nav aria-label="Primary navigation">
    {{ page.primary_menu }}
  </nav>
</header>

<main id="main-content" class="site-main">
  <a class="skip-link" href="#content">Skip to content</a>

  {% if page.highlighted %}
    <div class="container">
      {{ page.highlighted }}
    </div>
  {% endif %}

  <div id="content" class="container">
    {{ page.content }}
  </div>
</main>

<footer class="site-footer">
  <div class="container">
    {{ page.footer }}
  </div>
</footer>

The variables such as page.header and page.content correspond to the regions declared in the .info.yml file. Place blocks into those regions through the Drupal administration interface. If a region is missing from the template, blocks assigned to it will not appear, which is a common issue when creating a theme manually.

Drupal’s naming conventions help you discover the right template. A node can use node.html.twig; a specific content type can use node--article.html.twig; a view can use templates such as views-view.html.twig or a more specific suggestion. Enable Twig debugging in development settings to see template suggestions in the HTML comments and browser source.

Template or asset Typical responsibility Useful customisation
html.html.twig Document wrapper and <head> output Language attributes, body classes, metadata placement
page.html.twig Site-wide page regions Header, navigation, main content, footer
node.html.twig Full content entities and teasers Article structure, author details, publication date
field.html.twig Field label and item rendering Image captions, badges, grouped values
block.html.twig Reusable block wrappers Component classes, headings, landmark roles
Component CSS and JS Visual and interactive behaviour Cards, menus, accordions, buttons

Twig should present values rather than perform complex business logic. Use filters such as |escape, |clean_class, and |render appropriately, and let preprocess functions prepare data that needs transformation. Drupal automatically escapes most printed variables, but raw output should be treated carefully, especially when content is supplied by editors or external integrations.

Add Responsive And Accessible Components

A theme becomes maintainable when it is built from small components rather than a single large stylesheet. Define patterns for buttons, cards, alerts, navigation, forms, tables, and media. Use predictable class names and keep component-specific rules close together. A content editor should be able to add a promotional block without knowing how the entire homepage is structured.

Menus need special attention. A mobile navigation control should expose its state to assistive technology, use a real button, and update aria-expanded. A simple JavaScript behaviour might look like this:

(function (Drupal, once) {
  Drupal.behaviors.harbourMenu = {
    attach(context) {
      once('harbour-menu', '[data-menu-toggle]', context).forEach((button) => {
        const target = document.querySelector(button.dataset.menuToggle);

        button.addEventListener('click', () => {
          const expanded = button.getAttribute('aria-expanded') === 'true';
          button.setAttribute('aria-expanded', String(!expanded));
          target.hidden = expanded;
        });
      });
    }
  };
})(Drupal, once);

Use Drupal behaviours and once() so the script works after AJAX content updates as well as on the initial page load. Do not rely on hover-only interactions, colour alone, or tiny text. Check keyboard focus visibility, heading order, form labels, link purpose, error messages, and sufficient colour contrast.

Accessibility has practical legal importance in Australia. The Disability Discrimination Act 1992 can apply to websites and digital services, while WCAG provides a recognised technical benchmark. A government, education, health, or community website should treat accessibility as a delivery requirement from the beginning rather than a visual polish task at the end.

Images should have meaningful alternative text, decorative images should use empty alternative text, and responsive image styles should be configured in Drupal rather than forcing every visitor to download the largest original file. This improves performance for regional visitors and helps manage hosting and bandwidth costs.

Configure Development And Test The Theme

Turn on Twig debugging only in a development environment. In a Composer-based project, development configuration is commonly kept in sites/development.services.yml, while production settings disable verbose errors and template suggestions. Rebuild caches with Drush after changing Twig files, theme metadata, region definitions, or libraries:

vendor/bin/drush cr
vendor/bin/drush theme:enable harbour
vendor/bin/drush config:export

The exact Drush commands can vary with project setup, but the principle remains the same: theme discovery and rendered output are cached. If a new template seems to have no effect, clear caches first and verify that the filename matches Drupal’s template suggestion.

Test the theme at several viewport sizes and with realistic content. Long Australian place names, local phone numbers, postal addresses, event dates, and currency values can expose layout problems that short placeholder text hides. Check addresses from Perth to Hobart, and test dates using the format expected by the organisation’s audience rather than assuming a United States format.

Before deployment, check page speed, image payloads, JavaScript errors, broken links, and form behaviour. Test with keyboard navigation and a screen reader, then inspect the site in current versions of Chrome, Firefox, Safari, and Edge. A public site should also behave sensibly on touch devices and slower connections, not just on a fast developer laptop.

Maintain A Reliable Release Workflow

Keep the theme in Git with the rest of the Drupal codebase. Commit templates, CSS, JavaScript, image assets, and YAML definitions, but do not commit generated caches or environment-specific settings. Use a separate branch for design changes and review template modifications carefully because a small Twig change can affect every page.

A theme may contain configuration dependencies, such as a view display, image style, or block placement. Export configuration with the site’s normal workflow and test imports on a staging environment. Avoid making manual production changes that cannot be reproduced in code or configuration. This is especially important for agencies managing sites across Sydney, Canberra, and Brisbane, where a local staging copy may be used by several people.

Production settings should disable error details, protect private files, enforce HTTPS, and provide appropriate security headers through the hosting or reverse-proxy configuration. If the site collects contact details, newsletter subscriptions, analytics data, or account information, document the relevant handling practices under the Privacy Act 1988. Marketing email workflows may also need to account for Australia’s Spam Act 2003.

Use this release checklist before making the custom theme the default:

A well-structured Drupal theme remains replaceable: content stays in entities and configuration, while presentation lives in documented templates and assets. That separation makes future redesigns, accessibility audits, mobile improvements, and Drupal core upgrades considerably easier to manage.