Replace Multiple Themes with One Theme (or Zero Themes)

Too many times I have seen clients with multiple themes with only minor differences between them. Taking advantage of Liferay Theme features can reduce your total number of custom themes down to one. And with 7.4's latest features, you might even be able to get to zero custom themes...

Introduction

So I was recently working with a team that was completing an upgrade assessment for a client wanting to upgrade from 7.2 to 7.4.

A big chunk of the work in upgrading the client was going to be taken upgrading the themes. The client had one primary theme which contained the common style rules, but they also had 8 "child" themes, based upon the primary, but with slight changes (think of them like brand changes).

As a very rough estimate (and because I'm not the best theme guy), for estimation I would typically say 3 weeks for the primary theme and then probably a week per child theme. This gives me time to get the theme right and working, get appropriate testing done, get client approval, etc. Since the child themes are minor changes to the parent theme, a week per child theme should be more than enough.

But based on this math, I'd be spending 11 weeks just on themes and not on anything else. Like I said, I'm not the strongest theme guy so this number would be reduced as the skills of the theme developer increase, but this is still going to take a lot of time to deal with 9 themes.

However, if we could reduce down from 9 themes down to only 1 theme, even with my adequate skills, I'd be dropping from 11 weeks down to 3 weeks. This would represent a significant savings in time, money, resources, future maintenance (one theme vs 9 themes), deployment effort, runtime resource consumption, ...

The good news is that Liferay offers a bunch of features which can be used to make this happen. Let's review them and see how we might put them to work.

Although I'm not really going to call them all out here, it is important to note that everything presented in this page is fully localizable. All of the keys, all of the lables, etc are all keys which will be processed by Liferay's Localization support. So in all cases I haven't shown any of the localization setup, but just following standard Liferay practices you can add localization support for all of your settings, frontend tokens, etc.

Theme Settings

The very first thing we can leverage are Theme Settings. Theme Settings are flags which can be set on the Configuration for the page(s) in the Design tab, but they are limited to use within the Freemarker templates for the theme:



These are all Liferay OOTB themes, but as you can see, the theme settings themselves are unique to the theme itself.

Declaring Custom Theme Settings

In your theme project, in src/main/webapp/WEB-INF/liferay-look-and-feel.xml (you may not have one, but you can create one, or you can build your theme and copy the one the build generates for you in build/buildTheme/WEB-INF), you'll find something similar to:

<?xml version="1.0"?>
<!DOCTYPE look-and-feel PUBLIC 
  "-//Liferay//DTD Look and Feel 7.4.0//EN" 
  "http://www.liferay.com/dtd/liferay-look-and-feel_7_4_0.dtd">

<look-and-feel>
  <compatibility>
    <version>7.4.0+</version>
  </compatibility>
  <theme id="custom" name="custom">
    <template-extension>ftl</template-extension>
  </theme>
</look-and-feel>

Our new <settings /> element will go underneath the <template-extension /> tag, and it is where we'll be adding the individual theme settings.

Your settings are going to follow the following sort of pattern:

<setting configurable="true" key="show-header" type="checkbox" value="true" />

The configurable attribute must be true to be displayed, otherwise it is not something that can be changed in the UI. The key is the language bundle key for the label to show for the setting, but it is also going to be the key for accessing the setting to use later on.

It's important to note though that you are not limited to checkboxes. When you check the DTD, you'll see that you have 4 different types to pick from: checkbox, select, text, or textarea.

If you pick the select type, then you can also provide the options attribute which is a comma-separated list of items (or language bundle keys) to pick from.

After adding some settings, we could find our look and feel defined like:

<?xml version="1.0"?>
<!DOCTYPE look-and-feel PUBLIC 
  "-//Liferay//DTD Look and Feel 7.4.0//EN" 
  "http://www.liferay.com/dtd/liferay-look-and-feel_7_4_0.dtd">

<look-and-feel>
  <compatibility>
    <version>7.4.0+</version>
  </compatibility>
  <theme id="custom" name="custom">
    <template-extension>ftl</template-extension>
    <settings>
      <setting configurable="true" key="show-header" type="checkbox" value="true" />
      <setting configurable="true" key="show-footer" type="checkbox" value="true" />
    </settings>
  </theme>
</look-and-feel>

Using the Settings in the Theme Templates

Like I wrote earlier, these settings are going to be visible only to your Freemarker templates, so you can't really use them on, say, changing or controlling CSS styles. You could, of course, have the FM template conditionally import one or more CSS scripts based upon theme settings, so you can accomplish a similar goal, but your logic is stuck in the FM template itself.

The typical usage pattern for theme settings is first to use the init_custom.ftl file to transform the settings into usable values.

For example, we would use the following for the two settings that have been defined:

<#assign show_header = 
  getterUtil.getBoolean(themeDisplay.getThemeSetting("show-header")) />
<#assign show_footer = 
  getterUtil.getBoolean(themeDisplay.getThemeSetting("show-footer")) />

The use of getterUtil will allow you to do transforms on types (from String to numerical, for example, or boolean in this case) including passing in default values in case there is no setting.

Once you have these as Freemarker variables, then you just use them in your Freemarker template as though it was any other context variable:

<#if show_header>
  <div>Show Header set to ${show_header}.</div>
</#if>

Theme Variants

Before talking about Theme Variants, I want to first introduce Color Schemes. Color schemes are a legacy throwback to Liferay 4 days, yet they are still completely valid, even for 7.4.

The original idea behind color schemes (and was actually implemented in the Liferay 4.3 Classic Theme) was that you could define a theme with a basic default color such as "Blue", but then you could define additional color schemes like "Orange" and "Green" and, through the addition of a class on the HTML elements and color scheme style sheets, you would override the colors.

This was back in the day when a portal was really hosting portlets only, and users had their own public/private pages and they might want to pick a color scheme that worked for them (i.e. I might use the Blue color scheme but you pick the Green one, but since it is based on one theme we get the same basic look and feel, only colors change).

Anyway, each color scheme would have a class name assigned to it, in this case they would be blue, green and orange, and when you check the markup you'd find it being set at the body level:

<body class="blue">
...
</body>

And when you changed the color scheme, the class would be changed.

<body class="green">
...
</body>

The CSS would then take these changes into account, so your main theme CSS would have rules like:

a:hover {
    color: #06c; /* this is a shade of blue */
    text-decoration: none;
}

Then in your green color scheme CSS you'd have an override rule:

.green a:hover {
    color: #0c0; /* this is a shade of green */
}

The higher specificity of the green class mean that this rule would supercede the default rule and apply the green color.

Because these were named as "color schemes" and the examples Liferay provided were all about changing color schemes, unfortunately this was all they ever were really applied for.

In reality, though, the implementation did nothing to limit what an individual color scheme CSS could override, they literally could have changed everything in a color scheme including margins, padding, fonts, sizes, etc.

A better name for this mechanism would have been Theme Variants and then provide some examples which actually did more than just change colors.

Defining a Theme Variant

To define a Theme Variant, aka Color Scheme, we need to add to the liferay-look-and-feel.xml file for the theme:

<?xml version="1.0"?>
<!DOCTYPE look-and-feel PUBLIC "-//Liferay//DTD Look and Feel 7.4.0//EN" 
    "http://www.liferay.com/dtd/liferay-look-and-feel_7_4_0.dtd">

<look-and-feel>
  <compatibility>
    <version>7.4.0+</version>
  </compatibility>
  <theme id="custom" name="custom">
    <template-extension>ftl</template-extension>
    <settings>
      <setting configurable="true" key="show-header" type="checkbox" value="true" />
      <setting configurable="true" key="show-footer" type="checkbox" value="true" />
    </settings>
    <color-scheme id="01" name="Unbranded">
      <default-cs>true</default-cs>
      <css-class>no-brand</css-class>
      <color-scheme-images-path>${images-path}/variants/${css-class}
      </color-scheme-images-path>
    </color-scheme>
    <color-scheme id="02" name="Brand-A">
      <css-class>brand-a</css-class>
    </color-scheme>
    <color-scheme id="03" name="Brand-B">
      <css-class>brand-b</css-class>
    </color-scheme>
  </theme>
</look-and-feel>

With this configuration, our regular theme has the name Unbranded and the CSS class of no-brand, whereas two additional variants are defined for Brand A and Brand B.

In the _custom.scss file, we need to remember to import our variant scss files, plus we can define rules just like we might have done in a regular theme:

@import 'variants/brand-a';

@import 'variants/brand-b';

#footer {
  color: #fff;
  margin-top: 1rem;

  > .container {
    padding-bottom: 3rem;
    padding-top: 3rem;
  }
}

And in css/variants/brand-a.scss we could override these settings:

.brand-a {
  #footer {
    color: #aaa;
    margin-top: 2rem;

    > .container {
      padding-bottom: 5rem;
      padding-top: 5rem;
    }
  }
}

The brand-b.scss would likewise have its own variation.

Using Theme Variations

After building and deploying our custom theme, we're now ready to use it.

On a vanilla 7.4 DXP Q3.2 release, when I edit the default welcome page and go to the Page Design Options under the gear menu, I can choose to use a theme for the page, change the theme and find my theme has been added to the list:

After selecting my theme, I can also now see that my Theme Variations (aka Color Schemes) are also available:


When I just use the Unbranded (default) variation on the page, I can verify it is applied by checking the DOM for the footer:


Note the no-brand class has been added to the <body /> tag, and the styling for the footer matches what we put into _custom.scss.

When I select the Brand-A variation, checking the DOM shows that it too is applied:


The brand-a class is now in the <body /> tag in place of no-brand, and I can see that my style rule was updated also.

Finally, I can do the same validation on variation Brand-B:


Again, same update to the class on the <body /> tag and same changes on the style rules.

Benefits of Theme Variants

I would argue that this alone is enough for the client we were working with to reduce from 9 themes down to one as it accomplishes the same goals:

  • The primary theme using the default Theme Variant has all of the necessary standard style rules set.
  • Additional Theme Variants can be defined to provide the kind of explicit rule changes the variant needs.

Plus there's the additional benefits the client would see:

  • Updating a single theme instead of possibly changing 8 or 9 different themes.
  • Simplification of the build/deploy process.
  • Significant reduction in the amount of time required to upgrade one theme vs 9 themes.

However, the one major drawback of using Theme Variants is that this is a developer activity. So the specs have to be generated, handed to the theme developer, theme developer makes the changes, builds one or more artifacts, those artifacts have to go through testing to verify they meet the requirements, then the artifacts need to be deployed to production (possibly during off hours or in a maintenance window and maybe triggering a temporary outage), ...

Theme Variants, although able to reduce us down to a single theme project, they're still a lot of development activity.

Fortunately there is a better way...

Style Books

Style Book support was added in 7.3 so they're kind of new to Liferay, but they are based on CSS Custom Properties which were introduced in the CSS Values and Units Module Level 4 specification at the end of 2015 and added to browsers in 2016/2017.

Style Books are extremely flexible solution (as we'll see shortly), but they do have a restriction: the theme must be applied to all pages. You can't pick a single page, use a different theme that supports a style book, then change its style book. The theme must apply to all pages, but then each page can use the same or a different style book (based on the theme). It's a special restriction to be aware of. You can pick a different Style Book for each page, you just can't use a custom theme with its own custom style book.

There are three aspects to CSS Custom Properties that we are concerned with during theme development:

  1. Definition
  2. Usage
  3. Runtime Configuration

Let's start with the first option and go from there.

Defining Frontend Tokens

For Style Book support in your theme, the first thing you need to do is define what are referred to as frontend tokens. The definition of a frontend token provide a name, a label, a type, an [optional] editor type, a default value and additional mapping information. The mapping information is critical for using the property as this is where the cssVariable is given the value. The value is going to be the actual name of the CSS custom property and therefore must be unique, so it's important to ensure you don't introduce conflicts between your custom property names.

Each frontend token represents a single CSS custom property, and these tokens can be organized into categories and subsets.

Categorization and token set organization will be an important aspect for editing Style Book values in the UI, so it is worth the effort to define and use this organization early on in theme development.

Frontend tokens are defined JSON in the src/main/webapp/WEB-INF/frontend-token-definition.json file. Fortunately Liferay provides a number of examples of this file for our reading pleasure:

From our Theme Variation example, we were effectively changing 3 things: The footer color, the footer margin top and the footer container children padding. Instead of doing these things as Theme Variations, let's use a Style Book approach for controlling these settings.

To support this, we need a frontend-token-definition.json file to define our tokens:

{ "frontendTokenCategories": [ {
  "name": "customStyles",
  "label": "custom-styles",
  "frontendTokenSets": [ {
    "name": "footerStyles",
    "label": "footer-styles",
    "frontendTokens": [ {
      "name": "footerColor",
      "label": "footer-color",
      "type": "String",
      "editorType": "ColorPicker",
      "defaultValue": "#fff",
      "mappings": [ {
          "type": "cssVariable",
          "value": "footer-powered-by-color"
      } ]
    }, {
      "name": "footerTopMargin",
      "label": "footer-top-margin",
      "type": "String",
      "defaultValue": "1rem",
      "mappings": [ {
        "type": "cssVariable",
        "value": "footer-top-margin"
      } ]
    }, {
      "name": "footerContainerPadding",
      "label": "footer-container-padding",
      "type": "String",
      "defaultValue": "3rem",
      "mappings": [ {
        "type": "cssVariable",
        "value": "footer-container-padding"
      } ]
    } ]
  } ]
} ] }

Sorry for the not pretty formatting, but as you can see the file can be quite verbose, especially when you have a lot of variables.

Remember that I called out organizing earlier? Well check out my poor job at organizing my items. I only used a single category, Custom Styles, and within there a better "Footer Styles" section. For such a contrived example this works, but if I were building a real theme I'd probably have to spend more time to plan this out.

Anyway I've defined my three tokens that control what I need to vary, so next it is time to use my tokens in the theme.

Using Frontend Tokens in Theme SCSS Files

To use the frontend tokens in our theme SCSS files, we have to follow a specific format.

In places where you might use a style explicitly, you're going to change to using var(--css-custom-variable) instead, where the css-custom-variable is replaced with the actual value that is defined in the token definition. Note the prefix on the variable name, the two dashes - those are required, so don't forget about them.

Going back to our custom theme, if we remove the theme variants and instead switch over to the CSS custom properties, we'll now have:

#footer {
  color: var(--footer-powered-by-color);
  margin-top: var(--footer-top-margin);

  > .container {
    padding-bottom: var(--footer-container-padding);
    padding-top: var(--footer-container-padding);
  }
}

A variation on this which you'll often see in Liferay SCSS implementations is:

#footer {
  color: $poweredByColor;
  color: var(--footer-powered-by-color, $poweredByColor);
  margin-top: var(--footer-top-margin);

  > .container {
    padding-bottom: var(--footer-container-padding);
    padding-top: var(--footer-container-padding);
  }
}

In order to take advantage of SCSS variables, the color line gets duplicated, the first line says to use the SCSS variable poweredByColor, then the 2nd declaration can apply only if the CSS custom property is defined, and the var() form will use the poweredByColor as the default if it doesn't have a default value.

Build and deploy our theme and we're ready to move to the UI...

First, the restriction mentioned earlier has to be addressed, so in the site we have to navigate to the Site Builder -> Pages section, then use the peapod menu in the upper right corner to get to the Configuration panel and then the Design tab. Here's where we set the default theme for the site, so we need to pick our updated custom theme:


Now doing just this, when we return to our main page and check out the DOM, we can see:


The theme is now set using the CSS custom properties, the color scheme class is gone (because I removed them from the custom theme), and because we set default values, the theme is showing the Powered By text in white.

Next though is where the fun begins...

Runtime Configuration

Since we now have a Style Book-enabled theme, we can start defining Style Books to represent specific CSS property values in a named entity. Being able to define a style book separately from the pages ensures that page admins can leverage the predefined style books but maybe not have the ability to create or customize a style book themselves.

If we navigate to the site's Design -> Style Books panel, we'll see that we do not have any style books defined, so let's create a new one and call it Unbranded:


As this is meant to showcase the defaults, we can just hit the publish button.

Let's next create the Brand A Style Book:


And we might as well also create the Brand B Style Book:


Now the list of Style Books is complete:


With these style books ready to go, now when you edit a content page and navigate to the Page Design Options panel and choose the Style Book tab, you can now pick from the available style books.

If you try this, just click through your various books and watch how the page changes to reflect the values set in the selected Style Book.

Benefits of Style Books

Like the Theme Variants, Style Books would allow us to reduce down to a single theme and accomplish the same goals:

  • The default values set on the frontend tokens has all of the necessary standard style rules set.
  • Using the Style Book editor, changes can be made to create the kind of explicit rule changes the brands need.

Plus there's the additional benefits the client would see:

  • Updating a single theme instead of possibly changing 8 or 9 different themes.
  • Simplification of the build/deploy process.
  • Significant reduction in the amount of time required to upgrade one theme vs 9 themes.
  • Styling changes can be made in the UI without a developer and are live right away, so no deployment, promotion, outages, etc.

If there's a downside to Style Books, I would say it comes in two areas:

First, it does take some extra work up front before you start your theme project. You kind of need to know which things you're going to support changing in the front end vs those things you won't support, and of course you need to figure out the best way to organize your frontend tokens so they'll be easy for the page admins to find the right properties and set the correct values.

Second, it does increase the complexity of your theme. Now you're not just adding classes and style rules, but you're also maintaining the frontend tokens (and their organization), so your development effort is going to increase somewhat as a result. However, when you consider the savings in time/effort when the page admin can simply make Style Book changes directly in the UI, I'd argue that this extra development effort is worth it.

Hitting The Trifecta!

It's important to note that you don't have to pick just one of these solutions as the only way, you can leverage all of them at the same time...

For example, imagine that you are responsible for a soft drink-branded merchandise website. You know that you have one major brand that uses a certain color of red and another major brand uses a certain color of blue.

Using the Theme Variation, you can define variations that include these differences in the variation itself. And if you think about it, this makes perfect sense because you wouldn't want some page admin changing the brand's red color to green by accident, right?

However, just because some things like the primary colors might be fixed, there are other aspects that you want to allow a page admin to control the styling on. Handling those with CSS custom properties and Style Books will add that flexibility to the theme and reduce the theme maintenance in the future.

And the theme settings, those are used by the Freemarker templates, so they are always an option when the Freemarker code needs to do different things on different pages.

So yeah, mix and match these techniques to build one custom theme that incorporate all of the variations and flexibility your requirements require.

The Only Winning Move is Not to Play

Sure, I just dug up an old quote from 1983's WarGames, but it is something to consider...

Rather than reducing from 9 to 1 themes, what if we could reduce from 9 down to 0 custom themes? Wouldn't that be a lot better?

Depending upon your requirements, it could be possible.

How might we do this? By leveraging the following items:

Use Liferay's Dialect Theme - The Dialect theme is a theme with an overabundance of Style Book support. Colors, margins, padding, fonts, sizes, etc are all defined as CSS custom properties. The frontend-token-definition.json file for Dialect has almost 8,000 lines and over 600 defined CSS custom properties. With this much flexibility baked into a single theme, unless you're building something on the fringe such as the new sesamestreet.com site for kids or something, it's a good bet that Dialect along with the right Style Book settings could entirely replace your theme.

Use Client Extensions (CX) - Liferay 7.4 includes support for a new feature called Client Extensions. There are specialized extensions for Global CSS and Theme-specific CSS. With these types of extensions, you're not building a complete theme, but you are providing CSS class/style overrides which will apply during page rendering. Although CX are thought of primarily as an LXC-only feature, they work just as well on LXC-SM as well as on-prem deployments.

Using this combo, you can leverage Dialect and Style Books to get most of what you need from a theme, and then leverage the CSS CX to control those things that you can't tweak with a Style Book setting.

Zero themes means no theme developers/development, no theme testing, no theme deployment, no theme upgrades on GA releases or U-bundles or quarterly releases, ...

Lots of benefits to this path if you don't have requirements that would prevent you from pursuing it...

Conclusion

First and foremost, if you're reading this and you have more than one theme in your workspace, I hope you're now thinking about pushing those together and creating just one theme.

That effort alone will save so much time, money, effort, maintenance, system resources, etc. that it will more than pay for itself in the long run.

And better yet, if you take the time and review the Dialect theme and the CSS CX, maybe you're thinking about eliminating your custom themes altogether, saving even more time, money, effort, ...

If you have any questions or suggestions, hit me up below or find me hanging out on the Liferay Community Slack!

Good luck!