Get an ad-free experience when you join the WebWash Premium Community.
Don’t forget to subscribe to our YouTube channel to stay up-to-date.
Enjoy an ad-free experience and access exclusive courses when you join the WebWash Premium Community.
Learn about PremiumAlready a member? Log in
Building a custom theme for Drupal Canvas requires integrating Tailwind CSS with Drupal’s component system. This tutorial demonstrates the process of creating a theme from scratch, setting up the build tooling, and developing components that work with the Canvas page builder.
In the video above, you’ll learn how to generate a theme using Drush, configure Tailwind CSS with Vite, create page templates with proper region handling, style elements using preprocessors, and build Single Directory Components for Drupal Canvas.
Understanding the Project Structure
Boost Your Web Development Skills
Get lifetime access to all live streams ad-free and a private site builder forum. Click here to join.
Drupal Canvas introduces a new entity type called Canvas Page that differs from standard content types. Canvas pages use components for layout and content, providing a visual page-building experience. The theme must support both Canvas pages and standard Drupal pages.
Theme Requirements
A Tailwind-based Canvas theme needs several key elements:
- Theme configuration files: The
.info.ymland.libraries.ymlfiles define regions and assets. - Build tooling: Vite compiles Tailwind CSS into production-ready stylesheets.
- Page templates: Twig templates handle region output and Canvas-specific layouts.
- Preprocessors: PHP functions add classes to blocks and menus.
- Components: Single Directory Components provide reusable UI elements for Canvas.
Region Planning
Start with only the regions your design requires. The example theme uses six regions:
- Header: Contains the site branding block.
- Primary menu: Holds the main navigation.
- Pre-content: Displays breadcrumbs, page title, and tabs for standard pages.
- Content: The main content area for Canvas pages and regular content.
- Footer: A three-column grid for footer content.
- Footer bottom: Copyright and secondary footer information.
Try the Tailwind Starter Site


Want to see this theme in action before building from scratch?
The WebWash Tailwind Starter Site provides a complete working example with everything configured.
The starter includes a site template recipe with demo content, a Tailwind theme, and several pre-built Canvas components ready to use.
Clone the repository to explore how all the pieces fit together, then use it as a foundation for your own projects or follow along with this tutorial to understand the implementation details.
After setup, log in and go to Content > Pages to edit the Home page and explore Canvas.
Generating the Theme
Use Drush Generate to create the initial theme structure rather than building files manually.
Running the Generator
Generate a new theme with the following command:
drush generate theme
When prompted, provide these values:
- Theme name: WebWash Tailwind (or your preferred name).
- Base theme: Set to false for a standalone theme.
- Breakpoints: No (handled by Tailwind).
The generator creates the theme directory with starter files. Remove unnecessary files like the default JavaScript and CSS directories that the generator includes.
Delete the css and js directories as they won’t be required.
Turn off CSS and JavaScript Aggregation and Caching
When developing a Drupal theme, it’s recommended that you turn off CSS and JavaScript aggregation and switch on Twig debugging.

- Go to Configuration > Performance and uncheck Aggregate CSS files and Aggregate JavaScript files.
- Then go to Configuration > Development settings and enable the following options:
- Do not cache markup
- Twig development mode
- Twig debug mode
- Disable Twig cache

Install New Theme
Go to Appearance and then click on Install and set as Default on the WebWash Tailwind theme.
Configuring the Info File
Update the .info.yml file to define your regions:
name: WebWash Tailwind
type: theme
base theme: false
description: A flexible theme with a responsive, mobile-first layout.
package: Custom
core_version_requirement: ^10 || ^11
libraries:
- webwash_tailwind/global
regions:
header: 'Header'
primary_menu: 'Primary menu'
pre_content: 'Pre-content'
content: 'Content'
footer: 'Footer'
footer_bottom: 'Footer bottom'
Setting base theme: false creates a standalone theme without inheriting from Stark or another base theme. This approach requires more work but provides complete control over the output.
Configure Block Regions
Now that the regions have been defined in the .info.yml file, you’ll need to add the blocks to the specific regions through Drupal’s backend.
Go to Structure > Block layout and add the following blocks to the regions.
Note: Content blocks such as “Quick links” will need to be created manually.
- Header
- Site branding
- Primary menu
- Main navigation
- Pre-content
- Status messages
- Help
- Page title
- Primary tabs
- Secondary tabs
- Primary admin actions
- Content
- Main page content
- Footer
- Footer
- Quick links (Content block)
- About Us (Content block)
- Footer bottom
- Copyright (Content block)
You can adjust which blocks appear in which regions depending on your needs.
Setting Up Tailwind CSS
Tailwind CSS requires Node.js tooling to compile utility classes into a production stylesheet. Vite serves as the build tool for this configuration.
It’s recommended that you delete the package.json file that comes with the theme when you first generate it.
Installing Dependencies
Navigate to your theme directory and initialize npm:
cd web/themes/custom/webwash_tailwind
npm init -y
Install Tailwind CSS and Vite as development dependencies:
npm install -D vite tailwindcss @tailwindcss/vite @tailwindcss/forms
NOTE: The @tailwindcss/forms package adds basic form styling, which Tailwind omits by default. It’s not required, but it gives form elements basic styling.
Configuring Vite
Create a vite.config.js file in the theme root:
import { defineConfig } from 'vite'
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
tailwindcss(),
],
build: {
outDir: 'dist',
rollupOptions: {
input: 'src/css/style.css',
output: {
assetFileNames: '[name][extname]'
}
}
}
})
This configuration takes the source CSS from src/css/style.css and outputs the compiled file to the dist directory.
Configuring the Source CSS
Create the source CSS file at src/css/style.css:
@import "tailwindcss";
@plugin "@tailwindcss/forms" {
strategy: "base";
}
@source "../../templates/**/*.twig";
@source "../../components/**/*.twig";
@source "../../**/*.theme";
The @source directives tell Tailwind which files to scan for utility classes. Tailwind only compiles classes that appear in these files, keeping the output small.
As you add custom CSS components, you’ll need to add them into src/css/style.css.
@import "./components/buttons.css";
@import "./components/tabs.css";
@import "./components/typography.css";
Adding Build Scripts
Update package.json with build scripts:
{
"type": "module",
"scripts": {
"dev": "vite build --watch",
"build": "vite build"
}
}
Run npm run dev during development to watch for changes. Run npm run build for production builds.
Updating the Libraries File
Configure the theme to load the compiled CSS:
global:
css:
theme:
dist/style.css: {}
Creating the Page Template
The page template controls the overall page structure and must handle both Canvas pages and standard Drupal pages differently.
Canvas Page Detection
Add a preprocess function to detect Canvas pages:
function webwash_tailwind_preprocess_page(array &$variables): void {
$route_name = \Drupal::routeMatch()->getRouteName();
$variables['is_canvas_page'] = $route_name === 'entity.canvas_page.canonical'
|| str_starts_with($route_name, 'canvas.api.layout');
}
This function sets a variable that the template uses to conditionally show or hide regions.
Building the Template
Create templates/page/page.html.twig with conditional region handling:
{% if page.header or page.primary_menu %}
<header class="bg-white shadow-md">
<div class="container mx-auto px-4">
<div class="md:flex items-center justify-between py-4">
{{ page.header }}
{{ page.primary_menu }}
</div>
</div>
</header>
{% endif %}
{% if page.pre_content and not is_canvas_page %}
<div class="container mx-auto p-4">
{{ page.pre_content }}
</div>
{% endif %}
{% set section_classes = is_canvas_page ? 'p-0' : 'p-4' %}
{% set container_classes = is_canvas_page ? '' : 'container mx-auto px-4' %}
<main>
<a id="main-content" tabindex="-1"></a>
<section class="{{ section_classes }}">
<div class="{{ container_classes }}">
{% if page.content %}
{{ page.content }}
{% endif %}
</div>
</section>
</main>
<footer class="bg-gray-800 text-white py-8">
<div class="container mx-auto px-4">
{% if page.footer %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{{ page.footer }}
</div>
{% endif %}
{% if page.footer_bottom %}
<div class="border-t border-gray-700 mt-8 pt-6 text-center text-gray-400">
{{ page.footer_bottom }}
</div>
{% endif %}
</div>
</footer>
Canvas pages hide the pre-content region and remove the container wrapper from the main content area. This allows Canvas components to span the full viewport width.
Styling with Preprocessors
Add Tailwind classes to Drupal-generated elements through preprocess functions rather than overriding templates.
Styling the Site Branding
Add classes to the branding block:
function webwash_tailwind_preprocess_block__system_branding_block(array &$variables): void {
$variables['attributes']['class'][] = 'text-2xl font-bold text-gray-800 w-[200px]';
}
Styling the Menu
Add flex layout classes to the main menu:
function webwash_tailwind_preprocess_menu__main(array &$variables): void {
$variables['attributes']['class'][] = 'flex space-x-6';
}
Styling the Tabs
Add a custom class for tab styling:
function webwash_tailwind_preprocess_block__local_tasks_block(array &$variables): void {
$variables['attributes']['class'][] = 'ww-canvas--tabs';
}
Creating Tailwind Components
Define reusable styles in component CSS files for elements like buttons and tabs.
A Drupal site needs basic styling. We’ll add styling for typography such as headers and paragraphs, buttons, and tab links.
Tab Styles

Create src/css/components/tabs.css:
@layer components {
.ww-canvas--tabs {
& ul {
@apply flex flex-wrap gap-2 border-b border-gray-200 mb-6;
}
& li {
@apply m-0;
}
& a {
@apply inline-block px-4 py-2 text-sm font-medium text-gray-600 hover:text-blue-600 hover:bg-gray-50 rounded-t-lg transition-colors duration-200 cursor-pointer;
}
& a.is-active {
@apply text-blue-600 bg-blue-50 border-b-2 border-blue-600;
}
}
}
Button Styles

Create src/css/components/buttons.css:
@layer components {
.button {
@apply inline-flex items-center justify-center px-4 py-2 rounded-md font-medium transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer border-2 border-gray-300 bg-white text-gray-700 hover:bg-gray-50 hover:border-gray-400 focus:ring-gray-500;
&:hover {
@apply no-underline;
}
&.button--primary {
@apply bg-blue-600 text-white border-blue-600 hover:bg-blue-700 hover:border-blue-700 focus:ring-blue-500;
}
}
}
Typography Styles

Create src/css/components/typography.css:
@layer base {
h1 {
@apply text-5xl font-bold leading-tight mb-6;
}
h2 {
@apply text-4xl font-bold leading-tight mb-5;
}
h3 {
@apply text-3xl font-semibold leading-snug mb-4;
}
h4 {
@apply text-2xl font-semibold leading-snug mb-4;
}
h5 {
@apply text-xl font-medium leading-normal mb-3;
}
h6 {
@apply text-lg font-medium leading-normal mb-3;
}
a {
@apply text-blue-600 underline hover:text-blue-800 transition-colors;
.text-white &,
[class*="text-white"] & {
@apply text-white hover:text-gray-200;
}
}
p {
@apply mb-4 leading-relaxed;
&:last-child {
@apply mb-0;
}
}
}
Once these CSS components have been created, go ahead and update the src/css/style.css file and import the styles.
@import "./components/buttons.css";
@import "./components/tabs.css";
@import "./components/typography.css";
Make sure you run npm run dev or npm run build.
Building Canvas Components
Single Directory Components (SDC) provide reusable UI elements that Canvas can render. Each component lives in its own directory with a YAML definition and Twig template.
Creating a Text Component
Generate a component using Drush:
drush generate sdc
The text component definition at components/text/text.component.yml:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Text
status: stable
props:
type: object
properties:
text:
type: string
title: Text
contentMediaType: text/html
examples:
- <p>This is some text.</p>
align:
type: string
title: Text Alignment
description: The text alignment
enum:
- left
- center
- right
default: left
examples:
- left
The text template at components/text/text.twig:
{% set align_class = {
'left': 'text-left',
'center': 'text-center',
'right': 'text-right',
}[align] ?? 'text-left' %}
<div class="text-content {{ align_class }}">
{{ text|raw }}
</div>
Creating a Heading Component
The heading component allows you to add H1s and H2s into Canvas.
The heading component definition at components/heading/heading.component.yml:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Heading
status: stable
props:
type: object
properties:
heading_text:
type: string
title: Heading Text
default: 'Enter title'
examples:
- 'My Heading'
level:
type: number
title: Heading Level
description: The heading level (h1-h6)
enum: [1, 2, 3, 4, 5, 6]
default: 2
examples:
- 2
align:
type: string
title: Text Alignment
description: The heading alignment
enum:
- left
- center
- right
default: left
examples:
- left
The heading template at components/heading/heading.twig:
{% set align_class = {
'left': 'text-left',
'center': 'text-center',
'right': 'text-right',
}[align] ?? 'text-left' %}
{% set level = level|default(2) %}
<h{{ level }} class="{{ align_class }}">
{{ heading_text }}
</h{{ level }}>
Creating a Hero Image Component
The hero image component allows you to add full width hero images with headings, texts and buttons.
The hero image component definition at components/hero_image/hero_image.component.yml:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Hero image
status: stable
props:
type: object
properties:
media:
$ref: json-schema-definitions://canvas.module/image
title: Background Image
description: Background image for the hero
type: object
heading_text:
type: string
title: Heading Text
examples:
- 'Heading text'
subtitle:
type: string
title: subtitle
examples:
- 'Subtitle text'
link_url:
type: string
title: Link URL
examples:
- '/path'
link_label:
type: string
title: Link label
examples:
- 'Click here'
The hero image template at components/hero_image/hero_image.twig:
<section class="text-white py-34 relative">
{% if media.src is not empty %}
<div class="absolute inset-0 w-screen h-full z-[-1] ms-[-50vw] left-[50%]">
<img src="{{ media.src }}" alt="{{ media.alt }}" class="w-full h-full object-cover"/>
<div class="absolute inset-0 bg-black/40"></div>
</div>
{% endif %}
<div class="container mx-auto px-4 relative z-10">
<div class="max-w-3xl">
<h1 class="text-4xl md:text-5xl font-bold mb-4">{{ heading_text }}</h1>
<p class="text-xl mb-8 text-white">{{ subtitle }}</p>
<a href="{{ link_url }}" class="inline-block bg-white text-blue-600 px-6 py-3 rounded-lg font-semibold hover:bg-gray-100 no-underline">
{{ link_label }}
</a>
</div>
</div>
</section>
Creating a Card Component
The card component demonstrates media handling and multiple props:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Card
status: stable
props:
type: object
properties:
media:
$ref: json-schema-definitions://canvas.module/image
title: Card Media
description: Media and Alt for the card.
type: object
title:
type: string
title: Title
examples:
- 'Card title'
summary:
type: string
title: Summary
examples:
- 'Card summary'
link_url:
type: string
title: Link URL
default: 'https://www.example.com'
examples:
- 'https://www.example.com'
link_label:
type: string
title: Link label
The card template at components/card/card.twig:
<div class="flex flex-col bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden">
{% if media.src is not empty %}
<div class="overflow-hidden">
{% block media %}
<img src="{{ media.src }}" alt="{{ media.alt }}" class="w-full" />
{% endblock %}
</div>
{% endif %}
<div class="p-6">
{% if title is not empty %}
<h3 class="text-xl font-semibold text-gray-800 mb-3">{{ title }}</h3>
{% endif %}
{% if summary is not empty %}
<p class="text-gray-600 mb-4">{{ summary }}</p>
{% endif %}
{% if link_url is not empty and link_label is not empty %}
<a href="{{ link_url }}" class="text-blue-600 hover:text-blue-800 font-medium">{{ link_label }}</a>
{% endif %}
</div>
</div>
Creating a Section Grid Component
Layout components use slots to accept child components. The section grid provides column options:
'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/10.1.x/core/modules/sdc/src/metadata.schema.json'
name: Section grid
status: stable
props:
type: object
properties:
title:
type: string
title: Title
description: section title
highlight:
type: boolean
title: Highlight
description: Add light grey background to highlight the section
default: false
columns:
type: string
title: Columns
description: Grid column layout
default: '33-33-33'
examples:
- '33-33-33'
enum:
- '100'
- '50-50'
- '33-33-33'
- '25-25-25-25'
slots:
grid:
title: Grid
The section grid template uses Twig mapping for responsive grid classes:
{% set grid_class = {
'100': 'grid-cols-1',
'50-50': 'grid-cols-1 lg:grid-cols-2',
'33-33-33': 'grid-cols-1 lg:grid-cols-3',
'25-25-25-25': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
}[columns] ?? 'grid-cols-1 lg:grid-cols-3' %}
<section class="py-16 {{ highlight ? 'bg-gray-300' : '' }}">
<div class="container mx-auto px-4">
<h2 class="text-3xl font-bold text-gray-800 mb-8 text-center">{{ title }}</h2>
<div class="grid {{ grid_class }} gap-6">
{% block grid %}
{% endblock %}
</div>
</div>
</section>
Using Drupal Canvas
Installing Drupal Canvas
Install Canvas using Drush to avoid UI-related installation issues:
composer require drupal/canvas
drush en canvas -y
NOTE: As of this writing, you’ll need to install Drupal Canvas through Drush and not the Extend page. You may get a fatal PHP error.
Create Canvas Page
A new “Pages” tab appears under the Content section. This tab lists all pages managed by Drupal Canvas, separating them from traditional content types. Pages created with Canvas don’t appear in the standard content listing.
To access Canvas-managed pages:
- Navigate to Content > Pages.
- View all Canvas pages in the listing.
- Click Edit to open the Canvas interface.
You can also access Canvas pages directly by navigating to /canvas on your site.
Or if you’re in Drupal Canvas, click on Pages and then click on New > New page.
Components
Click on the add icon in Drupal Canvas and you should see all of the components that were added to the theme grouped under Other.
Simply drag the component onto the canvas page and then edit the component properties from the right-hand side.
For a detailed explanation on how to use the Canvas UI, check out the tutorial on Drupal CMS version 2 and Drupal Canvas.
Summary
Creating a Tailwind CSS theme for Drupal Canvas involves several steps:
- Theme generation using Drush provides the initial file structure.
- Vite and Tailwind configuration enables utility-first CSS compilation with tree-shaking.
- Page templates handle both Canvas pages and standard Drupal pages with conditional logic.
- Preprocess functions add Tailwind classes to Drupal-generated elements without template overrides.
- Single Directory Components integrate with Canvas to provide reusable, configurable UI elements.
Start with a minimal set of components and expand as your design requirements grow. Reference the Mercury theme from Drupal CMS for examples of production-ready Canvas components.





2 comments
Carlo
Really enjoyed this Video/ Instructions.
I remember when you had the old website WW Course, which included on how to build a site.
Please demonstrate basic site building instructional video. Use the KISS principle ( keep it simple stupid).
tks
Carlo
ps any chance doing as short video on pre-processing functions?
Ivan Zugec
Thanks for the comment, Carlo.
I do have a few ideas for site building live streams, but Drupal is in a transition period right now. The way you built sites six months ago isn’t how you’d build them today with Canvas and UI Suite. I want to make sure any tutorial reflects current best practices.
As for preprocessing functions, I’ll add that to the list.
But in the meantime, check out these links because preprocessors now can be implemented in a class:
– https://www.drupal.org/node/3442349
– https://www.drupal.org/node/3496491