Over the past few months, I've fielded a lot of minor styling requests, and as I work through each problem, I've almost always ended up with substantially less code than was there before. When I look through our portfolio of sites, most of them use tons of CSS to style all the various parts of a site -- and in many cases, the same styling reappears a bunch of different places. This leads to inconsistent styling, slow page loads, and more effort needed to make minor changes.
This is a form of "Technical Debt". It comes from starting with a poorly documented, not-fully-understood starting point for a theme, and grows through each change built upon this poor foundation.
Now this is not a dire problem. We've worked with hundreds of sites built in similar ways, each one with slightly different quirks and gotchas, and they all work. Mostly. If you don't look too closely at the details.
Making changes to these sites that work and look good across all screen sizes is a game of Whack-a-mole. Our first line of defense has been a solid "visual regression testing" system, which runs on every release for all the sites we manage. We pick up pixel-level changes to our themes -- for the pages we have in our list to test. But all too often, our themers have chosen to limit the scope of their changes, targeting changes to only specific locations that the client requests, instead of making the bigger decision to define something new that intentionally changes the entire site.
What we're doing now
We just kicked off a theme sprint for a new upgrade project, and we're doing it much differently this time. The vast majority of our theme work up until now has been done in CSS. We occasionally update a Twig template to add some information we want to include, or change the format of a date, or render an image as a background image -- but the vast majority of customizations have happened in SASS files we compile to CSS.
This time, we're flipping that. We've added TailwindCSS to the theme, and plan to make most of our customizations in the twig (HTML) templates and preprocess functions, instead of in custom CSS.
What is Tailwind?
Tailwind is a CSS framework that provides a ton of HTML classes that essentially are thin wrappers for individual CSS. It's a bit like using "style" tags inside your HTML elements, but labeling them as classes with shorter names. Now if you've been working with websites for a while you might've just retched a little -- sorry about that! But I've been using Tailwind on various presentations and front-end work for the past few months, and now I find myself cringing when I see deeply nested CSS selectors, and CSS definitions overriding other CSS overriding 3 more CSS definitions -- no wonder we have sites approaching a megabyte of CSS, or more!
With our previous approach, theming meant inspecting code, identifying CSS changes to make to change it to the way we want, and adding those by indirectly identifying a way to target a particular element, and then change the CSS stylesheets to add that change.
With Tailwind, instead of changing the CSS, you change the HTML -- you go straight to the template where you want to make the change, and add Tailwind classes to make the change.
But you're not done, in either scenario. What about mobile? Tablet? Super wide screens? With the old system, we would then either have to find the appropriate part of the stylesheet that has the responsive rules -- or, more often, create new ones. What was that breakpoint to target again? Did we have a Sass mixin to handle the breakpoint, or do we just hard-code in a pixel width? Programmers are lazy -- all too often we just put in what seems to work and look good to us, without going back to any kind of source or non-existent plan, and using the style this particular site used. And boom, now we have created more technical debt.
With Tailwind, all the positioning, margin, padding classes take prefixes for responsiveness. But, more than that, all tailwind classes can be prefixed with a breakpoint, to have that class only apply at screen sizes bigger than that breakpoint.
The end result is you have one place to go in the HTML DOM to change any style you want for that element: the list of classes on that element itself.
If you do this with CSS selectors, you may have a dozen different rules, a dozen different places in your tree of CSS or Sass files, that have an impact on the style of the very same element.
Why we think this will be better
Our goals for this change are:
- Make the theming process go faster
- Improve the quality and consistency, particularly on mobile
- Make the site load substantially quicker, with much smaller CSS files
- Make future changes far easier to incorporate, with the same level of quality
Gotchas, problem areas
We are not doing away with custom CSS entirely. One of the great things about Drupal is the separation between information and presentation. So we're not hopping entirely on the Tailwind bandwagon -- if we were to start using Tailwind classes inside content, that would lead to even more mess, when different editors choose different classes to add to specific chunks of text -- which could lead to wildly inconsistent look and feel of pages on your site.
Tailwind is not an editor tool! Tailwind is for developers only.
This is the balance to strike: Rapid, reliable development, versus editorial ease and ability to use components.
Drupal 10's CKEditor5 now supports a list of styles editors can quickly select to apply particular styles to text, as they edit. (This isn't new, but it is something we highly recommend using). We've experimented with adding a set of Tailwind classes to this dropdown, with a label -- but then realized this is going down the same path as WordPress with Gutenberg -- the presentation is getting stuffed into the content, which leads to much bigger challenges if you want to reskin the site, redefine the styling for a particular element.
So a key consideration here is, if you decide down the road to redefine what a "Highlight box" looks like on your site, how much manual work will you have to do to change everywhere you've used one?
If you define a "Highlight Box" as a single CSS class, you can just update that class. If you define it as a set of Tailwind classes, you'll need to go through all your content to find that set of classes and update them to the new look.
This leads us to our new...
Implementation Principles
We've identified several parts of this theme puzzle:
- Defaults - header styles, font styles, base size, base colors
- Layouts, margins, paddings, responsiveness
- Components - nested structures that can be reused
- Styles
Each of these has a different implementation strategy.
Defaults
Defaults can be specified in the Tailwind config file, along with classes on the body tag. Also, remember CSS variables -- CSS variables are a really powerful way to provide variations that use the same template. Define all the overall defaults first, along with base styles.
- Add default colors, font families, and typography size to the tailwind.config.js.
- Define base header styles in a base layer as described here.
Layouts, margins, paddings, responsiveness
If it has to do with boxes, it should be specified in a template. This is where we apply Tailwind the most -- on the Twig templates in the main theme. For the most part, use Tailwind here as much as possible, following the reasoning they outline for the framework itself.
Components
Here's where the grey areas come in. What is a component? Components are building blocks, often composed of other building blocks. In Drupal terms, it might be a block, a form, a specific set of nested HTML, a form element, a menu, or any number of different elements. This is the big new change we're making -- defining these individually.
Some are predefined, coming from Drupal core or various contributed modules. Some are self-contained widgets. There's a huge variety of different possible components.
This is where documentation comes in, and we've found a great place to document these: The Simple Style Guide module. This creates a section in the admin menu where you can drop in the appropriate HTML for a component as an example, and document where the CSS is implemented.
So, as a general rule, when theming a component, avoid putting Tailwind classes into HTML that will end up in the database. Here's where we need to come up with a distinct name for the component, and define a CSS class to style it appropriately.
Where does this CSS go? Depends on where the component is being defined. If it's being defined in the theme, the best practice would be to put it in the main.css file, and use @apply to define the CSS using other Tailwind classes. Many components are defined by Drupal core or various contributed modules, though -- if it's code we "own", go ahead and update the CSS where it is defined. If we don't "own" the code, then go ahead and override the styling in the theme CSS, again using @apply.
How does an editor use a component? Primarily by placing a block, specifying a class name for a block, or something in layout builder. In some cases these become templates dropped into Views in a global text field, or inserted as HTML -- we're still working out best practices here.
The actual list of component classes should be kept as small as possible. And, instead of creating different class names for slight variations of a component, use another set of classes for the variation. For example, a card with several different color variations is still a card -- add a class for the variation to use, and have that variation set CSS variables that the component CSS uses.
Styles
Finally, we reach individual text styles. These are options added to the Wysiwyg dropdown to allow editors to highlight text, turn links into buttons, or other "simple" components (components that do not have nested HTML elements). These should get defined in a separate "ckeditor5.css" stylesheet, and added to the "Styles" dropdown in the text format configuration.
Theme documentation
This is the critical part to keep technical debt low: Document your theme decisions as you go. This post is our guide to the various moving parts -- defaults, templates, components, and styles. We're not fans of extra documentation or creating extra work -- the goal here is to put notes where you will find them when you come back years later.
First and foremost: There should be a README.md file at the top level of the theme, with the instructions for doing a build and where to find the specific documentation. This is the index that should point a new developer touching this site later to the right place.
Documentation for the defaults is the tailwind.config.js file, and the "Base" layer of the main.css file -- use comments in these files where the defaults themselves are not obvious.
Documentation for the templates are in the Twig templates themselves. Turning on Twig debugging makes it easy to find a template being used -- in that template, drop in the classes and any notes to explain things that are non-obvious.
Documentation for Components and Styles should go in the Simple Styleguide in the site itself -- this gets exported to configuration, so it can stay with the site. The description for each component should identify what module or theme defines the component, provide instructions for editors or site builders to use the component, and point themers to any locations they need to go to style the component or style.
Getting started
We're making use of the new Starter Theme functionality recommended for Drupal 10:
php core/scripts/drupal generate-theme my_theme
... and using Laravel Mix with PostCss, with a setup based on a blog post by Matt Glaman:
cd web/themes/my_theme
npm install -D tailwindcss@latest postcss@latest
npm install -D laravel-mix
npx tailwindcss init
touch webpack.mix.js
Next, add the build scripts to the package.json so you can build for production with npm run build
, or watch your changes with npm run watch
:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "mix --production",
"dev": "mix",
"watch": "mix watch"
},
Configure Laravel Mix
Here we're still basing on mglaman's configuration, but have made a couple specific changes:
// webpack.mix.js
let mix = require('laravel-mix');
mix
.disableSuccessNotifications()
.postCss('./src/ckeditor5.css', 'build', [
require('tailwindcss')
])
.postCss('./src/main.css', 'build', [
require('tailwindcss'),
// require('autoprefixer'), - enable this if too many conflicts with Drupal CSS -- prefixes everything with tw-
])
.browserSync({
proxy: 'mysite.freelock.net',
files: [
'build/main.css',
'templates/**/*',
'my_theme.theme',
]
})
You may or may not want to use the autoprefixer extension, which makes it so you need to prefix all the Tailwind styles with "tw-".
Configure Tailwind
Mglaman's tailwind.config.js is for an older version of Tailwind -- with a current Tailwind 3.x version, there are some differences:
/** @type {import('tailwindcss').Config} */
module.exports = {
corePlugins: {
preflight: false,
},
content: [
'./templates/**/*.html.twig',
'./js/**/*.js',
'./my_theme.theme'
],
theme: {
/* Defaults for typography, colors, etc go here */
extend: {},
},
plugins: [],
safelist: [
/* Add any specific tailwind classes to always include. */
]
}
The secret sauce of Tailwind is how it parses the files listed in the "content" key for tokens, and evaluates those at build time to include only the classes you actually use into the built CSS. Its tokenization process is relatively dumb -- it just parses all of the matching files for space- or quote-delimited words, and any that match a Tailwind pattern end up in the build/*.css files. We go ahead and include the .theme file to pick up any classes added in a preprocess function, along with any Javascript files or Twig templates.
Configure the Drupal theme files
With the configuration above, running a build will put CSS files into a ./build directory in the theme, based on the CSS added in a ./src directory.
To load these into the browser, they need to get added to the my_theme.libraries.yml file, and the CKEditor-specific file needs to be added to the my_theme.info.yml file:
# my_theme.info.yml
...
ckeditor5-stylesheets:
- build/ckeditor5.css
libraries:
- my_theme/base
- my_theme/messages
- core/normalize
# my_theme.libraries.yml
base:
version: 1.0
css:
component:
...
theme:
build/main.css: {}
build/ckeditor5.css: {}
Note that we are still using the starterkit CSS files in ./css/components, so we keep those in the base library.
Main CSS file
Finally, we get to where you actually start adding CSS! For the time being, this is going in a ./src directory
/* ./src/main.css */
@tailwind base;
/* Put default CSS here. Don't wrap with @layer base {} here, because some elements may not be in templates alone. */
@tailwind components;
/* Put component CSS here - again, skip @layer component {}. */
@tailwind utilities;
... when this file gets longer than a few hundred lines, split out components into component files and @include them.
Build for production
... And now, to make it live, run npm run build
, clear the Drupal cache, and load your page!
Sounds great! I gave a talk…
Sounds great! I gave a talk on a similar approach at DrupalCon a year or two back: https://www.youtube.com/watch?v=MYY1teFZ_Fk
This looks like a great talk…
This looks like a great talk! Thanks for adding, I'll check it out...
Using TW classes in Views
You can add something like '../../../config/**/*.yml' to your tailwind.config.js to pick up any class used in Views, too. You do need to change the prefix separator to something besides ':' as this is currently invalid for class names in Views and they won't save properly (I use '--'), and then just run drush cex so the classes get exported to configuration where they'll be picked up and included. Still developer-centric, but now you don't have to make as many variations for Views templates. YMMV, of course.
Nice tip! I've run across…
Nice tip!
I've run across this restriction in block_class as well -- it sounds like using a ':' character in a class name violates the RFC for class names. I worked around this on one site by patching core to allow colons, but changing the separator sounds like a better fix.
But some of this led to my decision to keep Tailwind classes out of the Drupal admin interface -- don't use in content or config. If you do start adding these to views, block classes, etc. then you still may have to rebuild your CSS anyway, so this is where I'm going ahead and adding a specific class to the main.css file, and then using Tailwind classes in its definition using @apply.
Add new comment