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.
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:
- A component-specific local variable, empty by default.
- A theme variable, which impacts most components, empty by default.
- 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.
// This
:root {
--pfe-tabs--BorderColor: purple;
}// Not this
pfe-tab {
--pfe-tabs--BorderColor: purple;
}
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:
font-size: var(--pf-global--font-size, 16px));
^ theme variable ^ fallback value
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.
margin: var(--pf-cta--Margin, 3px);
^ local variable ^ fallback value
Or go basic, and simply code the spacer without any variables, if it’s not needed.
margin: 3px;
^ value only
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.
// Sass:.foo {
color: pfe-var(ui-accent);
}// CSS:.foo {
color: var(--pfe-theme--color--ui-accent, #06c);
}
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.
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.
$LOCAL-VARIABLES: (
BackgroundColor: transparent,
highlight: pfe-var(ui-accent)
);
If the value in the map is just a string, it will just print the plain fallback:
background-color: var(--pfe-tabs--BackgroundColor, transparent);
But if the value is a pfe-var
function, it will print both the theme variable, and then the fallback value.
border-bottom-color: (--pfe-tabs--highlight, var(--pfe-theme--color--ui-accent, #e00));
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:
pfe-tabs {
--pfe-theme--color--ui-accent: pink;
}
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.
color: pfe-local(Color);
Assuming you have a key value pair of color: #003366;
in your $LOCAL-VARIABLES
map, the compiled CSS will look like this:
color: var(--pfe-cta--Color--focus, #003366));
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
.
color: pfe-var(ui-base--hover--on-dark);
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!