Basic Tailwind CSS Theme Setup for Drupal Canvas

Watch this ad-free

Get an ad-free experience when you join the WebWash Premium Community.

Learn More

Don’t forget to subscribe to our YouTube channel to stay up-to-date.

Join the Community

Enjoy an ad-free experience and access exclusive courses when you join the WebWash Premium Community.

Learn about Premium

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.yml and .libraries.yml files 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.

  1. Go to Configuration > Performance and uncheck Aggregate CSS files and Aggregate JavaScript files.
  2. Then go to Configuration > Development settings and enable the following options:
  3. Do not cache markup
  4. Twig development mode
  5. Twig debug mode
  6. 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.

  1. Header
    • Site branding
  2. Primary menu
    • Main navigation
  3. Pre-content
    • Status messages
    • Help
    • Page title
    • Primary tabs
    • Secondary tabs
    • Primary admin actions
  4. Content
    • Main page content
  5. Footer
    • Footer
    • Quick links (Content block)
    • About Us (Content block)
  6. 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:

  1. Navigate to Content > Pages.
  2. View all Canvas pages in the listing.
  3. 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

  • 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?

    • A

      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

Leave your comment

✦ WebWash Premium

Go deeper with Premium

Lifetime access to every premium course, the private member forum and member-only live streams.

16+ in-depth courses
Private member forum
Member-only live streams
Ad-free, forever
Get lifetime access · €99

One-time payment · plus VAT