Theming hooks: Using CSS variables to trick-out your web components

CSS variables, a.k.a. custom properties, are a fantastic way to allow web components to be easily themed by page authors.

Kendall Totten
PatternFly Elements
7 min readAug 25, 2020

--

PatternFly Elements web components have a standard way of stacking variables and fallback values to provide default colors, but additionally we include “theming hooks”, or places for developers who are using these components to change colors, spacing, typography and more.

For example, here’s a CSS property within a the pfe-cta web component. The browser will look for these CSS variables in the order they are defined. If a variable is empty, it will ignore it and move onto the next one.

So the browser will look for these things, in this order:

  1. A component-specific local variable, empty by default.
  2. A theme variable, which impacts most components, empty by default.
  3. A fallback value, in case both variables are empty. This is the default.

Empty local variables

Leaving local variables empty means there is less specificity needed to override them. For example, in order to override the active tab highlight color within the pfe-tabs component, you’d only need to set a new value of a variable at the :root level instead of having to specify a CSS selector. This is desirable because using a CSS selector requires knowledge of which HTML tag or class to use, and it can create specificity battles.

When building web components, remember that CSS variables have a performance impact, so we should err on the side of only including them when necessary. If a local variable is not needed, your CSS can be simplified to use a theme variable only, then a fallback:

Or if a component has unique spacing between two items, for example the 3px between the call-to-action text and the arrow, then a theme variable is not necessary. In this case, you could include a local variable only, should you want that to be a hook for overrides.

Or go basic, and simply code the spacer without any variables, if it’s not needed.

Awesome Functions

You might think this sounds like a lot of extra work when building the component, but fortunately we have some helpful functions to do the heavy lifting for us.

Watch a demo video of how to use the pfe-local and the pfe-var functions to easily build the variables stack!

How does the pfe-var function work?

The pfe-var function returns that useful CSS variable stack for you, both with the correctly named theme variables and fallback value which is stored in the one of the pfe-sass/maps.

For example, asking the function to go look up the ui-accent color for you is so much easier than hunting through a list of variables to find the right one.

If you don’t happen to know the name of the color you need, you can browse the documentation page, or take a peek at the /_temp directory inside of pfe-styles, assuming you are compiling locally. This will be pulling values directly from the sass maps, so it’s guaranteed to be accurate.

pfe-styles/_temp/pfe-colors.css is a great source of truth.

How does the pfe-local function work?

The pfe-local function is a great workhorse that does several things. It builds a variable stack beginning with a local variable (using the key from the $LOCAL-VARIABLES map), which has no value, or is empty; this is important to allowing designers and developers using the web components to be able to create customizations. Next it looks to the $LOCAL-VARIABLES map, usually found at the top of the component file, to find out what value(s) to print as a fallback.

If the value in the map is just a string, it will just print the plain fallback:

But if the value is a pfe-var function, it will print both the theme variable, and then the fallback value.

Pro tip: add @include pfe-local-debug; to any component sass file to print the full list of local variables.

If, at this point, you’re convinced this sounds like a good idea overall, but when you are theming a component, it can be tricky to know when to add local variables. You can start by asking some questions:

Does normal CSS (from the page) already do the job?

PatternFly Elements strives to allow components to inherit all typography properties (font-family, font-size, font-style, font-variant, font-weight, line-height, etc.) from the normal CSS cascade, as these properties may be inherited even by shadow DOM elements. — source, CSS Tricks

Check out this great codepen by @castastrophe to see this in action.

Here are some additional rules of thumb:

  • If you are providing other styles for items within the Shadow DOM that may need to be overridden, you must also provide hook(s) via CSS custom properties.
  • Slotted content should not be directly styled by the component (i.e. the content areas within a card, tabs, or accordion, etc).
  • There is also a set of default styles you can choose to include with your components as a part of the pfe-styles component. One of the stylesheets, pfe-base.css, provides some global light DOM styles which may be useful.

Does the theme layer already do the job?

Some variables do not exist at the theme level and will need to be created locally within the component. For instance, if you would like to make a component width customizable, you’d probably need to create a special local variable for that, as there likely would not be an appropriate theme variable. However, there are many theme variables available, so be sure to check first.

Remember, page builders *can* scope global theme variables to a particular component if needed:

However it’s worth noting that if you override a theme-level variable, even scoped to a particular component, it will still cascade down to any nested components, so be cognizant of whether or not that is desirable. If the component is likely to contain other components, that could be a good reason to add a local variable.

One instance of a commonly needed variable is the background-color of a particular component. If you override the theme level variable:
--pfe-theme-color-surface-complement: blue; that would impact many components since its a theme level color . So you may want to make it easier to override the one component color independently, such as
--pfe-icon-BackgroundColor: blue; which will only impact the icon component.

Should this property be customizable at all?

This might be a conversation with the design team. Try to assess what an author might need to change in order to make the component look like the rest of their page. Usually color is a big one, but again, they may be able to use theme-level variables to achieve that.

A component like pfe-badge for instance, has unique colors which are not derived from the theme, so maybe unique variables are needed in case the color for “warning” matches the author’s branding, they would need to change that.

Consider, though, that one of the goals of PatternFly Elements web components is to create page UI elements that are visually similar. So if everything is customizable, you may lose traction in this area.

Does the component have variants or states that need to redefine a property ?

The pfe-cta is a good example of a component with attributes like pfe-type and pfe-variant. Since these attributes need to change multiple values at once, it makes it easier to expose these properties as CSS variables, and redefine the values (instead of re-defining the properties over and over). This also has an impact on specificity, which is intentional.

To set the value of a property to a local variable with Sass functions, you can utilize the pfe-local function, and then pass in the name of the variable defined in the component’s $LOCAL-VARIABLES map, such as color.

Assuming you have a key value pair of color: #003366; in your $LOCAL-VARIABLES map, the compiled CSS will look like this:

To set the value of a property to a theme variable with Sass functions, you can utilize the pfe-var function, and then pass in the name of the variable defined in any of the pfe-sass/maps such as ui-accent or ui--border-width.

And that’s it! Massive kudos to @castastrophe for writing the awesome Sass mixins and functions that make all this much easier within PatternFly Elements.

If you have questions or feedback, we’d love to hear about it, either here on Medium or in the PatternFly Elements issue queue. Thanks for reading, and be sure to check out the other blog post about how CSS “Broadcast” Variables” make for some contextually-aware components. Cheers!

--

--

Kendall Totten
PatternFly Elements

Engineering Manager at Pinata. Design geek who loves @Arcio, photography, design, music, travel & food. bento.me/kendalltotten