Creating a custom Drupal theme with Tailwind CSS
Creating a custom Drupal theme with Tailwind CSS gives development teams a flexible way to deliver a distinctive front end while preserving Drupal’s editorial workflow. Twig templates control the structure, and utility classes keep spacing, typography, colour and responsive behaviour consistent. When styles are organised around reusable components, the theme remains easier to extend than a collection of one-off page templates.
This walkthrough uses Drupal 10 or 11, Tailwind CSS 3.x, npm and the Stable base theme. It covers theme metadata, asset libraries, template overrides, component templates and release checks. The workflow suits a solo developer in Brisbane as well as a distributed team across Sydney and Melbourne, where repeatable builds in a shared repository are safer than copying changes between workstations.
Australian content also introduces practical requirements: English date formats, Australian dollar pricing, +61 telephone numbers and GST-inclusive totals. A commerce site may need different delivery promises for Sydney, Melbourne and Brisbane instead of a generic next-day claim. Encoding those rules in reusable content components prevents inconsistency when editors update nodes, Views or promotional blocks.
Setting up the Drupal theme foundation
Create the theme at web/themes/custom/australian_tailwind and keep its machine name identical to the folder name. Add the core metadata in theme.info.yml:
name: 'Australian Tailwind'
type: theme
base theme: stable
description: 'A responsive Drupal theme using Tailwind CSS.'
core_version_requirement: ^10 || ^11
libraries:
- australian_tailwind/global
regions:
header: 'Header navigation'
primary: 'Main content'
footer: 'Footer'
Stable provides a deliberately plain starting point without imposing a strong visual identity. Template overrides can replace its markup wherever the project requires a different structure. Region names should describe genuine layout areas, as predictable names such as header, primary and footer make administrator settings and theme overrides easier to maintain.
Declare global assets in libraries.yml so Drupal can aggregate, cache and order them correctly:
australian_tailwind:
global:
css:
theme:
css/tailwind.css: {}
Because the library is referenced in theme.info.yml, Drupal attaches the stylesheet to every relevant page automatically. This avoids hard-coded asset paths and lets Drupal append cache-busting query strings during deployment. If the theme later needs additional CSS or JavaScript files, add them to the same library definition rather than inserting unmanaged <link> or <script> elements into templates.
Installing and configuring Tailwind CSS
Install Node.js LTS, then initialise the build inside the theme directory:
cd web/themes/custom/australian_tailwind
npm init -y
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init
Configure Tailwind to scan Twig files that may contain utility classes:
module.exports = {
content: [
"./templates/**/*.twig",
"../../../core/modules/**/templates/**/*.twig",
"../../../core/themes/**/templates/**/*.twig",
"../../../modules/**/templates/**/*.twig",
],
theme: {
extend: {},
},
plugins: [],
};
The scan paths run relative to the theme’s tailwind.config.js. Tailwind detects complete class names, so classes assembled dynamically in Twig can be missed. Keep important utilities as literal strings or add narrowly scoped safelist entries where configuration genuinely requires dynamic values.
Create assets/css/tailwind.css with Tailwind’s three processing layers:
@tailwind base;
@tailwind components;
@tailwind utilities;
Add npm scripts for development and production. The watch command rebuilds CSS as templates change, while the build command creates the minified file expected by libraries.yml at css/tailwind.css. Run the production build before activating the theme, otherwise Drupal can request a stylesheet that does not exist.
Different integration methods suit different hosting environments:
| Integration method | Best suited to | Main advantages | Main limitations |
|---|---|---|---|
| npm and PostCSS | Active development teams | Versioned dependencies, repeatable builds and minified output | Requires Node.js in the deployment pipeline |
| Precompiled CSS | Restricted hosting environments | Deploys without Node.js and gives precise cache control | A maintainer must rebuild and publish the file |
| CDN script | Rapid prototypes | Fast setup with no local build | Version control, caching and content security policies require care |
A committed npm build is the strongest option for a maintained production site. The CDN route is useful for proving a concept, but it is less predictable when Drupal aggregation, strict browser policies and long-term upgrades are involved.
Translating the design into Twig templates
Drupal discovers template overrides in the custom theme, allowing html.html.twig and page.html.twig to replace the corresponding Stable templates. Keep the document template responsible for metadata and foundational elements, then use semantic page markup for the main content area:
<main id="main-content" class="mx-auto w-full max-w-7xl px-4 py-8">
<h1 class="text-3xl font-bold text-slate-900">{{ page_title }}</h1>
<div class="mt-6">
{{ page.content }}
</div>
</main>
Semantic elements provide a more reliable foundation than utility classes applied to generic containers. Headings, navigation landmarks, lists and buttons should retain their native meaning, while Tailwind controls their presentation. The skip link, page title and main landmark should also remain available even when a bespoke layout is introduced.
Twig’s auto-escaping protects variables unless a trusted render array is deliberately marked as safe markup. Avoid using the raw filter merely to solve malformed output, since that can expose unsafe user content. Views, menus, media and field output should remain Drupal render arrays wherever possible, allowing the presentation layer to apply formatting without replacing Drupal’s caching and security behaviour.
Use hook_preprocess_page() to expose route-specific values, such as a content type label or editorial metadata, to templates. Keep database access in services and leave heavy transformations to preprocess functions or dedicated normalisers. This separation allows an editor to change a field or View without requiring a front-end rewrite, while developers can inspect the preprocessing layer for deprecated APIs during Drupal upgrades.
Building reusable, accessible components
Create small Twig templates for repeated interface pieces instead of copying markup into node, taxonomy and View templates. A promotional card can receive its title, body, image and destination through template context, allowing the same component to appear on campaign pages and content listings:
<article class="overflow-hidden rounded-xl border border-slate-200 bg-white">
{% if image %}
<img src="{{ image }}" alt="{{ image_alt }}"
class="aspect-[4/3] w-full object-cover">
{% endif %}
<div class="p-5">
<h2 class="text-xl font-semibold text-slate-900">{{ title }}</h2>
<div class="mt-2 text-slate-700">{{ content }}</div>
<a href="{{ url }}"
class="mt-4 inline-flex rounded-md bg-sky-700 px-4 py-2 font-medium text-white">
{{ link_text }}<span class="sr-only">: {{ title }}</span>
</a>
</div>
</article>
Responsive utilities should describe layout changes rather than individual devices. A grid using grid gap-6 md:grid-cols-2 lg:grid-cols-3 can move from one column to multiple columns without tying the design to a specific phone or monitor. Images still require sensible dimensions, meaningful alternative text and consistent focal points at every crop.
Accessibility needs to be built into the component rather than checked at the end. Maintain sufficient colour contrast, visible keyboard focus, adequate touch targets and reduced-motion behaviour. Many Australian public-sector briefs reference WCAG 2.2 AA or Australian Government accessibility guidance, so the theme should be tested with keyboard navigation and a screen reader before editors begin publishing through it.
Australian formatting should also remain configurable. Store prices and telephone numbers in appropriate fields, then display them through Drupal’s locale and field settings. The interface can use en-AU, $ or AU$, DD/MM/YYYY and international +61 formatting without hard-coding those conventions into Twig. For delivery copy, use only service promises the business can meet in Brisbane, Melbourne, Sydney and regional areas.
Testing, deploying, and maintaining the theme
Build the CSS, enable the theme and clear Drupal’s cache before testing:
npm run build:css
drush theme:enable australian_tailwind
drush cache:rebuild
Test the finished pages as an anonymous visitor, an authenticated subscriber, an editor and an administrator. Cover content types, taxonomy terms, menus, search, media, contact forms and Views rather than checking only the homepage. Open the theme at narrow mobile widths, on a large desktop display and with browser zoom enabled. Keyboard order, focus visibility, headings, form labels and error messages should behave consistently throughout.
The deployment pipeline should run npm ci followed by npm run build:css before copying the Drupal code into the release artifact. Keep node_modules out of the deployed site, and commit package.json, package-lock.json, the Tailwind configuration, source CSS and Twig templates. If the hosting platform cannot execute Node.js, publish the generated stylesheet through the release process instead.
Tailwind’s content scanning removes unused utilities, but image sizes, font delivery and JavaScript still affect page performance. Australian users outside major metropolitan centres may rely on variable fixed connections or mobile data, making stable layouts and efficient assets particularly valuable. Review core Web Vitals, compress responsive images, self-host fonts where appropriate and keep custom JavaScript limited to interactions that CSS cannot provide. Each Drupal or Tailwind upgrade should also pass Twig logging checks and a fresh editorial regression test before the release reaches production.