PBS 182 of X: CSS 'Variables' (CSS)
Cascading Style Sheets, or CSS, was among the very first technologies covered in Programming By Stealth. We started our CSS explorations way back in instalment 6, and by instalment 10 we’d pretty much finished with the topic. The only other time we covered CSS in any depth was instalment PBS 28, though we used it regularly throughout the series. We learned the fundamentals of how CSS’s works, and more importantly, why it’s important to use CSS to separate the look of web apps and websites from their semantic structure, which we’d previously learned is expressed in HTML.
Because our exploration of CSS never went beyond the basics, we never covered the more advanced features like so-called CSS Variables. Until Bootstrap 5.3 made it impossible to achieve certain customisations without CSS variables, I’d never encountered a need to learn about CSS ‘variables’. So, I didn’t make time to learn about them myself, let alone include them in this series. But now that we’re about to discuss customising Bootstrap, we have a good motivation for adding them to our proverbial programming toolboxes.
While Bootstrap customisation is our reason for exploring the topic, nothing we cover in this instalment is in any way specific to Bootstrap. The features covered here are completely generic and can be used on any of your web apps or web pages, whether or not they’re styled with the help of Bootstrap.
Note on the use of ‘colour’ &
colorin this instalment — as an Irish resident and proud European, I make a point of writing these notes in my own voice, using my local spellings and language patterns. Specifically, that these notes are written in Hiberno-English, or in HTML jargon,lang="en-IE". This is why the descriptive parts of this blog post spell ‘colour’ the European way, with a ‘u’. Computer programming languages, on the other hand almost universally written in American English (oren-US), and that very much includes CSS. Every colour-related identifier in the CSS specification uses the American spelling, without theu, for example,background-color. To avoid confusion, I choose to adopt the same American spelling for all my own identifiers when writing CSS. This is why all code snippets in these notes usecolor.
Matching Podcast Episode
You can also Download the MP3
Read an unedited, auto-generated transcript with chapter marks: PBS_2026_04_11
Note on PBS 181 Challenge
There was a challenge set at the end of the previous instalment, but since that challenge is related to Jekyll, we’ll park it for a few more instalments. We’ll use the challenge as preparation for resuming our exploration of Jekyll, so the final instalment before we return our focus to Jekyll will end with a reminder about the challenge. We will then start the first new Jekyll instalment with the challenge solution.
Instalment Resources
- The instalment ZIP file — pbs182.zip
Reminder — CSS Basics
CSS style sheets define how HTML content should be rendered — the sizes of things, the typography, the colours, the arrangement and spacing of the various elements on the page, the borders to be added to the various elements, and so on.
The fundamental building blocks of CSS are:
- Style Declarations (also referred to as Style Rules) — definitions for style properties applied to different parts of the page using selectors.
- Style Properties — every HTML element on a page has a list of specific pieces of relevant style information associated with it. These are the visual properties CSS controls. For example, the element’s colour, the type face to be used for any text within the element, and how much padding there should be around the element.
- Selectors — a syntax for targeting style declarations at specific HTML elements within a page. For example, the selector
papplies declarations to all paragraphs,#waffleapplies declarations to just the element with the IDwaffles, andp.pancakesapplies declarations to all paragraphs with the classpancakes.
For each style property the specification defines a set of characteristics, most importantly:
- Applies to — the subset of HTML elements the property affects.
- Initial value — the default value for the style property.
- Inherited — whether the style property does or does not cascade by default (see below).
These building blocks are applied to HTML pages following two fundamental principles:
- In general, values for style properties cascade into nested HTML elements — that is to say, when there is no value assigned for a specific property on a specific HTML element, the value for that property is inherited from the element’s parent element. This rule applies at every level, causing style property values assigned to outer HTML elements to flow, or cascade into all the HTML elements they contain.
- Given how declarations cascade and how selectors inevitably overlap, there will often be competing possible values for specific style properties on specific HTML elements. These conflicts are resolved through a well defined order of precedence, referred to as specificity — for each property, the value used will be the one from the selector with the highest specificity.
Note that, like physical electrons flow in the opposite direction to the more abstract concept of electric current, style property value inherit up, but we describe styles as cascading down!
The vast majority of style properties cascade by default, the notable exceptions being the properties related to layout: margin, border, padding, etc..
The reason properties are defined as having a default inheritance behaviour rather than an absolute inheritance behaviour is that the specification allow it to be overridden. The mechanism for this over-ride is a set of special values that can be assigned to any CSS property within style declarations:
inherit— forces inheritance for that property on the selected elements.initial— forces the property’s default value to be used to be used for the selected elements. But beware, the documentation warns that setting an inherited property toinitialis best avoided because the initial value can be ‘unexpected’! As a general rule, it’s probably best to useunsetinstead (see below).unset— restores default cascading behaviour for the property on the selected elements. For inherited properties that means forcing inheritance, and for un-inherited properties that means forcing the default value.
Note there are another two, more advanced, special values for controlling inheritance,
revertandrevert-layer, but they’re beyond the scope of this series.
The Problem to be Solved
The feature colloquially referred to as CSS variables was added to CSS to solve a common problem that we have been side-stepping by using Bootstrap’s default styles for all our web apps — consistency without code repetition!
We’ve achieved consistency by simply applying the Bootstrap CSS classes to our various elements, and trusting that Bootstrap’s style sheet will keep all our spacing, typography, and colours consistent, which of course it does.But had we been developing our own custom styles from scratch, we’d be all to familiar with this problem!
For example, were we to have chosen to use the green from the Bartificer Creations logo for success rather than Bootstrap’s default green, we would have needed tens, or perhaps even hundreds, of style declarations that explicitly assign the value #93c020 to various elements. Now imagine we also wanted to use the orange from the Bartificer Creations logo as our colour for warnings, now we would have another few tens or hundreds of statements hard-coding #f19200. Just having all those magic (meaningless) values would make our style sheets difficult to read, debug, and maintain. Worse still, imagine we needed to change our minds about one of those colours?
Clearly, we need to be able to define some form of reusable named values in our style sheets! In any other programming language we would call these named values variables.
How CSS Implements ‘Variables’ — CSS Custom Properties
CSS is not a programming language in the traditional sense — you don’t execute CSS code to achieve tasks! Instead, it’s a kind of markup language for defining how HTML content should look. Given this context, it shouldn’t be surprising that CSS’s approach to variables is nothing like what we’ve seen in traditional programming languages like JavaScript and Bash.
The way CSS implements the concept of custom named values is by extending its existing concept of style properties. Simply put, we can invent our own CSS style properties! The properties we invent behave just like the regular CSS style properties we use all the time like color, font-family, border-width, etc.
However, because we’re inventing these properties ourselves, they have no direct visual effect. For them to become somehow visible, we need to reference them from within style declarations for the traditional CSS style properties. In effect, we say set the background colour to be the same as the local value for this invented property.
Local value? Like every other CSS style property, every HTML element gets its own internal instances of our invented properties, and, just like with traditional style properties like font-family, the values for our invented properties can cascade through our document.
In other words, CSS variables behave like CSS properties, not like JavaScript variables, because they actually are CSS style properties!
Finally, custom CSS properties cascade by default.
Using CSS Custom Properties
The CSS specification actually defines two related mechanisms for defining custom properties — a simpler one that’s sufficient for many situations, and a more complex but powerful additional optional mechanism.
Custom CSS Property Names
Like with traditional variables, there are rules around how CSS properties can be named.
The rules are simply:
- Custom property names must start with two dashes, i.e.
-- - And must follow all the same rules as traditional CSS property names like
background-color.
In effect that means case-sensitive dash-delimited names of the form --my-variable-name.
By convention, custom properties are named following the same approach as the standard CSS style properties — full words in all lower case, separated with dashes. You could use upper-case letters if you wished, but your properties would look very unusual, and other developers would probably get cranky with you!
Assigning and Using Custom CSS Property Values
Other than their funny names, you assign values to custom properties just like you would for the traditional CSS properties — that is to say, using style declarations.
To set the color property to DarkBlue for all top-level headings, we would use:
h1 {
color: DarkBlue;
}
So, you can set the value for a custom property named --pbs-logo-color to #00408d on every top-level heading with:
h1 {
--pbs-logo-color: #00408d;
}
If you choose to do this, the custom property --pbs-logo-color will only have a value on <h1> tags, and that value will be inherited by the child tags of those <h1> tags, and their child tags, and so on. In other words, the value gets directly assigned on all <h1> tags, and then cascades down into any tags nested within <h1> tags.
There are times you want to make these kind of fine-grain ‘variable’ assignments, but that’s not the most common problem to be solved. What you’re much more likely to want to do is assign a single value at the document level, and have that one value cascade down to every element on the page.
CSS defines a special selector that represents the document as a whole, :root, so to assign the value #00408d to the custom property --pbs-logo-color so it cascades to all elements we would use the following style declaration:
:root {
--pbs-logo-color: #00408d;
}
We now have a named value of our choosing available within every element. But, by definition, that custom property has no effect on the rendering of any elements. Like with traditional variables, simply defining them and assigning an initial value to them has no effect; we need to actually use our new variable!
To make use of a custom property we need to reference it within a traditional style definition for a standard style property.
To do this we need to make use of another advanced CSS feature we’ve ignored so far in this series — CSS functions!
We’ll look at functions in a little more detail below, but for now we just need to know that the var() function lets us access a custom property’s local value. This is a very simple function to use — simply pass the name of the the custom property whose value is needed as the only argument. For example, we can use the colour stored in our --pbs-logo-color custom property as a text colour with a style declaration like h1 { color: var(--pbs-logo-color) }.
Note that var() accepts an optional second argument, a fallback value to use when the system property is undefined. For example, the following declaration would set the text colour to the value of our custom property, or if it’s not defined, to black: h1 { color: var(--pbs-logo-color, Black) }.
Note that var() returns a specific value, not any kind of reference, so as the newly defined style cascades down into the child elements, the value that cascades does not get recalculated for each child!
What does that mean?
Imagine we have the following HTML code:
<p>This is the parent tag, <em>and this is a child tag</em>.</p>
Then imagine we have the following style sheet:
:root {
--waffle-color: GoldenBrown;
}
p {
color: var(--waffle-color);
}
em {
--waffle-color: BurntOrange;
}
What colour will the text “and this is a child tag” be? Golden brown, or burnt orange?
Let’s work it out.
What value will the color property have for the <em> tag? There is no value explicitly assigned to it, so it will inherit its value from its parent, the <p> tag. What value does the <p> tag’s color property have? The results of the function call var(--waffle-color), so what value does that custom property have for the <p> tag? Again, there is no value explicitly defined, so the value is inherited, in this case from the root, so for the <p> tag, --waffle-color is GoldenBrown, so, the <p> tag’s color is GoldenBrown, and that colour cascades down into the <em> tag’s copy of color, regardless of the fact that for that tag, the custom property --waffle-color is BurntOange!
If we wanted to use the value for --waffle-color within the <em> tag we would need to add another CSS declaration to set it directly, hence removing the implicit inheritance. For example, we would highlight the <em> tag with some padding and a border:
em {
--waffle-color: BurntOrange;
padding: 0.25em;
border-color: var(--waffle-color);
}
Note that we set the value of our custom property before we tried to use it — that’s vitally important, otherwise the inherited value would be used for the border-color line!
A Simple but Realistic Example
To see how the kind of ‘global variable’ we’ve just learned about would typically be used, let’s define some code to:
- Store the colour code for the PBS logo colour in a custom property named
--pbs-logo-colorat the root level, and let it cascade down into the document - Set the colour of all headings to the logo colour
- Set the background of all buttons to the logo colour too, and make their text white
Here’s the needed CSS code:
/* set the PBS logo color for the entire document */
:root {
--pbs-logo-color: #00408d;
}
/* set the colour of all headings to the logo colour */
h1, h2, h3, h4, h5, h6{
color: var(--pbs-logo-color);
}
/* set text colour of buttons to white, and the background colour to the logo colour */
button {
color: white;
background-color: var(--pbs-logo-color);
}
Advanced Custom Property Definitions with @property — Optional
For simple custom property use, you don’t need @property definitions, but they are useful to know about, and you may prefer their more explicit approach. I certainly prefer them over using the :root selector. There is an extra incentive to learn about them — Bootstrap 5 uses @property definitions extensively, and the variables defined in that way are the key to CSS-only Bootstrap customisation!
The syntax for @property definitions will look a little unusual because it is an example of another advanced CSS feature we’ve been ignoring to date — at-rules.
Unlike style definitions, at-rules don’t work in the element level, but the document level. They exist to provide global controls.
While we haven’t seen any at-rules in our own CSS code, we have implicitly used them thanks to Bootstrap, which uses @media at-rules to define the size-classes it uses for it’s responsive behaviours.
Somewhere else where you may have seen an at-rule is in the font embedding options offered by Google Fonts. When you request the code to import any of their fonts into a website they give you two options — <style> tags, or @import at-rules. We’ll be using this at-rule method to use custom fonts in our final worked example below.
But we’re digressing — let’s return our focus to custom properties by exploring how they can be defined at the document level with the @property at-rule.
The syntax for @property at-rules takes the following form:
@property --my-property-name {
syntax: "A VALID TYPE DEFINITION";
inherits: true; /* or false */
initial-value: "SOME VALUE"; /* optional */
}
The initial-value property acts as a default value, ensuring there at least a value for the custom property in every element.
Beware because at-rules are not regular style declarations, CSS functions can’t be used within their definitions!
This means initial values can’t reference the values of other custom properties. That does not mean you can’t have one custom property derive its value from another, you just can’t assign that value with the
@propertydeclaration. Usually, that means you simply declare a value at the root level using the:rootselector and let it cascade. You’ll notice this approach used in some of the examples below.
The inherits property is used to define whether or not the custom property being defined cascades. This property is not optional, the CSS specification forces you to make an explicit choice about your property’s inheritance!
The syntax property allows you to define the valid type or types for the values assigned to your custom property. Any style directives assigning invalid values will be ignored. You can use this explicit approach to stop accidental mistakes causing spooky action at a distance.
The full set of rules for the syntax property is surprisingly complex, allowing some really quite advanced usages, so we’ll constrain ourselves to a simplified sub-set.
Firstly, there are three kinds of type specifiers:
*— a special type to accept any valid CSS value.<TYPE_NAME>— any value fitting into a given broad class of value types, e.g.<color>,<length>,<number>,<integer>&<percentage>.SPECIAL_VALUE— a specific valid CSS special value likeauto.
Secondly, these kinds of types can be combined using three operators:
- A space-separated list of values is denoted by appending a
+to a type, for example, to accept a space-separated list of colours, use<color>+. - A comma-separated list of values is denoted by appending a
#to a type, for example, to accept a comma-separated list of integers, use<integer>#. - The
|symbol can be used to allow multiple kinds of values, like a boolean or, for example, to accept valid lengths or the keywordauto, use<length> | auto.
Finally, the full list of supported types can be found on this Mozilla Developer Network page, but here are some you’re likely to need:
<number>for any valid number, e.g.42,3.1415,-1, or-273.15.<integer>for any valid whole number, e.g.-38,0, or42.<length>for just about any valid CSS length value in absolute absolute relative units, but excluding percentages, e.g.42pxor2rem.<length-percent>the same as<length>, but including percentage values, e.g.42px,2rem, or50%.<string>for any string of text.<color>for any valid colour value, including colour names, e.g.white,#080808, or#000.<image>for any valid CSS value that can specify an image, generally that will be a URL.
Let’s put all this together by re-writing our logo colour example using an @property definition:
/* declare the logo colour variable and give it a default value */
@property --pbs-logo-color {
syntax: "<color>";
inherits: true;
initial-value: #00408d;
}
/* set the colour of all headers to the logo colour */
h1, h2, h3, h4, h5, h6 {
color: var(--pbs-logo-color);
}
/* set buttons to use the logo colour as their background, and to have white text */
button {
color: white;
background-color: var(--pbs-logo-color);
}
Note that setting an initial value with @property eradicates any need for fallback values because the custom property will always have a value, and fallbacks are only triggered when a custom property has no value.
Finally, to see this working for yourself, open the file pbs182-example1.html from the Instalment ZIP in your favourite browser. This file defines the above styles in the <head> section, and contains the following body:
<h1>PBS 182 — Example 1 (CSS Custom Properties)</h1>
<p>This examples uses as CSS custom property defined document-wide with an initial value to specify a colour to be used for all headings, and as the background for buttons.</p>
<p><button>A Button That Does Nothing</button></p>
We can use this file to illustrate how custom properties work by adding additional CSS statements inside the <style> tag.
Experiment 1 — Set the Variable to a Different Value on Some Elements
CSS custom properties behave like regular properties, so just like we can set different colours and border widths on different elements using CSS selectors, we can do the same with the value of our --pbs-logo-color custom property. Also, since we have explicitly configured out property to cascade down into child elements (inherits: true;) , the value should change on both the elements we explicitly target with our selector, and all the elements within those.
We can prove this by adding a style declaration that changes the value of our custom property to the very striking MediumVioletRed HTML colour on all paragraphs:
p {
--pbs-logo-color: MediumVioletRed;
}
Adding this to the end of the <style> tag and refreshing the page should change the background of the button to this new colour. Why? Because our custom property has been assigned a new value on every <p> tag, and that value has cascaded down into the <button> tag which is inside a <p> tag.
Experiment 2 — Set the Variable to an Invalid Value on Some Elements
Because our example tag uses an advanced @property definition, our custom property has been explicitly declared as only accepting colours. This means that if we try to set it to any other value, CSS will ignore that style declaration.
We can do that by editing our new selector to change the value for our custom property from MediumVioletRed to 42px, which is definitely not a valid colour!
Refreshing the page shows that our invalid assignment was ignored, so the button remains dark blue. Why? Because it is behaving exactly as if our invalid assignment did not exist.
Using Custom Properties with Functions for Computed Values (Optional)
Using custom properties directly is already powerful, but to really get the most from custom properties you need to combine them with CSS functions to compute related values from specified custom property values.
CSS function are too broad and advanced a topic to explore in detail here, so we’re going to limit ourselves to just the two functions you’re most likely to need. If this little preview whets your appetite, you can explore CSS’s full set of functions in this MDN article.
Before we explore out two sample functions, let's take a moment to look at the standard usage for CSS functions in general:
function-name(optional arguments)
/* or */
function-name(optional, arguments)
Yup, some functions expect their arguments to be space-delimited, and some comma-delimited. Just to be clear, it’s the function author who chooses how arguments are separated; users of the function don’t get to use whichever they feel like! Annoyingly, this means you always need to check the documentation for a function to understand how to pass arguments to it.
Note that the one function we’ve seen already, var() is an example of a function that expects comma-delimited arguments.
Function arguments can be any valid CSS value in any unit, and, they can be the outputs of other CSS function calls. That means we can use the var() function to use the value of a custom property as a function argument.
Basic Arithmetic with calc()
There are lots of reasons you might want to derive additional numeric values from a numeric value saved in a custom property, but consistent sizes and spacings are a very common use-case.
For example, to make your paddings and margins internally consistent and pleasingly proportioned, you could define a single base spacing size, and then use pleasing multiples of this base value throughout your style sheet. If you later decide you want more or less negative space in your layout, you can simply edit the master spacing size once, and all the spacings will adjust themselves proportionally, retaining your desired ratios. This is the approach Bootstrap takes — all the padding and margin utility classes like p3, m5 etc. are multiples of a single master value Bootstrap refers to as the space. To make Bootstrap feel more or less roomy, you simply edit this one variable and all the margins and paddings adapt.
The calc() function expects to be passed a list of space-delimited arguments that form a sequence of values and arithmetic operators that make mathematical sense. The values must be numeric, but they can be in any unit, and only the basic four arithmetic operators can be used, specifically +, -, *, and /. You can find detailed documentation on MDN.
As an example, let’s look at a simple implementation of the concept of using ratios of a base spacing size for all paddings and spacings:
/* set a base spacing value */
:root {
--space: 1rem; /* the width of one em-dash at the document root's font size (root-em)*/
}
/* calculate margins and paddings for the document */
body{
margin: 0px;
padding: 0px var(--space) 0px var(--space); /* top left bottom right*/
}
p {
margin-top: 0px;
margin-bottom: var(--space);
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0px;
margin-bottom: calc(var(--space) * 0.5);
}
blockquote { /* same vertical margin as paragraphs, but double the side margin and with padding */
margin: 0px calc(var(--space) * 2) var(--space) calc(var(--space) * 2); /* top left bottom right*/
padding: calc(var(--space) * 1.5)
}
Colour Mixing with color-mix()
Have you ever noticed how many professional websites seem to have a whole suite of colours that are all derived from just a small handful of base colours? Take Bootstrap for example; it works off just eight master colours named primary, secondary, info, success, warning, danger, light, and dark. All the colours you see in the various Bootstrap components are calculated from those base values. This makes it very simple to customise the colours in Bootstrap — you don’t need to specify each different shade to represent the foreground, background, light, active & disabled variants of the colours, you just specify the master colours, and Bootstrap calculates the rest.
If you understand colour theory you’ll be pleased to discover that the color-mix() function can work in multiple colour spaces. You can find all the details in this MDN document. I don’t understand colour theory well enough to use this function to its full effect, but I understand enough to generate different shades of the same colour using a very simple technique — mixing more or less of the master colour with white to generate a series of lighter shades.
For this approach to work I use the darkest shade I will need as my reference colour, then calculate the rest.
Here’s a very simple example generating just one very light background colour from a master colour:
/* set an accent colour — this will be the darkest shade used */
:root {
--accent-color: #00408d; /* the PBS shade of dark blue */
}
/* Use shades of the accent colour for quotations */
blockqote {
color: var(--accent-color);
background-color: color-mix(in srgb, var(--accent-color) 10%, white); /* calculate a suitably light background shade */
}
Worked Example
Let’s tie all this together with a worked example that makes use of all these techniques to build a demo page that:
- Defines a suite of custom properties for controlling the page’s presentation:
- Customisable fonts
- Customisable colours
- Customisable spacing
- Customisable styling of quotations
- Uses variables derived from other variables to reduce the number of variables the user needs to choose values for.
- Overrides the value of one of the variables on specific subsets of the page to demonstrate nesting — specifically, overrides the body font custom property within quotations.
The final worked example can be found in the instalment ZIP as pbs182-example2.html. Open that file in your favourite browser to see the final result, and in your favourite code editor to review the code.
Because the code is long, I won’t include it all here, instead, I’ll snip some of the most notable sections for quick reference.
The Page’s Structure
To understand how the CSS works, it’s important to understand the structure of page’s body. I’ve kept it simple enough to avoid confusion, but with enough complexity to demonstrate the key concepts.
The page has a top-level heading, paragraph, and block quote. The block quote contains nested paragraphs, one of which has a CSS class assigned, specifically, class="author". This is the page’s complete body:
<h1>PBS 182 — Example 2 (CSS Custom Properties)</h1>
<p>True wisdom is a rare and wonderful thing, and it should be cherished where ever it's found.</p>
<blockquote>
<p>"The world is a dangerous place to live; not because of the people who are evil, but because of the people who don't do anything about it."</p>
<p class="author">— Albert Einstein</p>
</blockquote>
The Style Sheet’s Organisation
The style sheet follows best practice and is arranged in the following order:
- The at-rules:
-
An @importrule to import three Google Fonts ([these three to be specific](https://fonts.google.com/share?selection.family=Indie+FlowerUbuntu+Mono:ital,wght@0,400;0,700;1,400;1,700 Ubuntu+Sans:ital,wght@0,100..800;1,100..800)) - The
@propertydefinitions for all custom properties
-
- The root-level assignments for the calculated custom properties
- The style declarations for that page which use both the directly defined custom properties and the calculated ones.
A nice feature in Google Fonts is that you can select multiple fonts and generate a single embed code to cover them all, so we only need one @import statement:
/*
* Import Desired Google Fonts:
* - Indie Flower (a handwriting font)
* - Ubuntu Mono (a monospaced font)
* - Ubuntu Sans (a sans-serif font)
*/
@import url('https://fonts.googleapis.com/css2?family=Indie+Flower&family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Ubuntu+Sans:ital,wght@0,100..800;1,100..800&display=swap');
For the most part the @property definitions are by-the-book — specifying valid types, enabling inheritance, and setting an initial value. However, the computed custom properties are a little different. Their definitions contain only syntax and inherits properties, for example, the accent background colour is simply defined as:
@property --accent-background-color {
syntax: "<color>";
inherits: true;
/* no initial value specified - will be calculated from the accent color */
}
The reason for this is that, as flagged above, functions can’t be used within initial-value options in @property declarations, so there’s no way to access the initial value of one custom property from within the definition of another. This restriction is easy to work around by adding a document-level style declaration that computes the value and allows it to cascade through the document.
For the accent background colour that means adding the following in addition to the @property declaration:
/* Calculate default values for the calculated custom properties */
:root {
/* calculate the accent background color from the accent color */
--accent-background-color: color-mix(in srgb, var(--accent-color) 10%, white);
/* … */
}
Notable Custom Property syntax Examples
To help illustrate the power of the syntax option in @property declarations, I intentionally chose which of the page’s features to make customisable. As a result, scrolling through the @property definitions near the top of the style sheet will show a nice selection of syntax definitions.
The first thing to note is that there is a wide variety of types used, specifically <string> for font names, <integer> for pixel and REM counts, <color> for colours, <length> for sizes in any unit (used for border widths & corner radii), and some CSS keywords for the border style. Notice I did not fall back to the generic * type, which I simply consider bad practice!
As well as a nice selection of types, I also made sure to use one example of each combinator — there are comma-delimited lists, space delimited lists, and a collection of allow-listed special values.
Let’s look at some of the most illustrative examples.
Firstly we have a comma-separated list of strings for the default body font family:
@property --body-font {
syntax: "<string>#";
inherits: true;
initial-value: "Ubuntu Sans", "sans-serif"; /* the quotation marks are not optional*/
}
We have an integer for the base font size in pixels. Note that there is no mechanism for defining valid ranges in CSS, which seems a likely opportunity for future enhancements, so for now, the most explicit we can be is that the pixel size must be a whole number:
@property --base-font-size-px { /* the base font size to use expressed in pixels */
syntax: "<integer>";
inherits: true;
initial-value: 16;
}
Next we have a space-delimited list of lengths in any unit for specifying the thickness of the four borders around quotations. Again, there’s no mechanism for limiting the number of items to the desired one or four (one to define all borders widths the same or four to define each separately), so the best we can do is require a list of space-delimited lengths:
@property --quote-border-width { /* the widge of all borders as a single value, or the top, right, bottom & left width */
syntax: "<length>+"; /* one or four space-separated widths */
inherits: true;
initial-value: 0px 0px 0px 5px;
}
And finally, we have an example of a list of allowed CSS keywords for the border style for quotations:
@property --quote-border-style {
syntax: "dotted|dashed|solid|double";
inherits: true;
initial-value: double;
}
Notable Style Definitions
Most of the stye definitions are pretty by-the-book, but one interesting technique I want to draw your attention to is the use of calc() for unit conversation.
The best example of this is the spacing around the quotation. The margin and padding are both derived from the --space-rem custom property, but that property is an integer, not a REM value. To convert it to a REM value we simply multiply the integer value by an appropriate amount of REM:
blockquote { /* add extra spacing around quotations */
padding: calc(var(--space-rem) * 1rem); /* needed with background colours and/or borders to move the text in from the edges */
margin-left: calc(var(--space-rem) * 2rem);
margin-right: calc(var(--space-rem) * 2rem);
}
Basically, in CSS, 2 * 2REM = 4REM!
Nesting a Custom Property
To illustrate the use of different values for the same custom property on different parts of a page, I chose to override the default --body-font within block quotes.
Updating the variable’s value on all block quotes and all tags within all block quotes is as easy as:
blockquote {
/* apply local changes to any needed custom properties before referencing them */
--body-font: "Indie Flower", "cursive";
/* … */
}
As explained previously, simply updating the value of the custom property has no visible effect. We need to use that value in a style declaration that has a high enough specificity to ‘win’ over the less specific declaration that has already set the font family to the default value of the --body-font custom property at the <body> level.
These two things can be done within a single css statement, if, and only if, the custom property is updated before it’s new value is used:
blockquote {
/* apply local changes to any needed custom properties before referencing them */
--body-font: "Indie Flower", "cursive";
/* set the styles, using the localised custom properties */
font-family: var(--body-font); /* add a more specific font-family declaration to 'win' against the one on the body */
/* … */
}
Tweaking the Variables
Now that we’ve explored how the sample solution works, let’s tweak the settings to see how the page reacts.
In the root: block within the style sheet you’ll find some commented out global custom property assignments that will override the initial-value of specific custom properties, allowing us to experiment with different values so we can see how extensively they affect the document’s overall style.
Experiment 1 — Increasing the Base Font Size
Un-comment the following line to change the base font size from 16px to 24px:
--base-font-size-px: 24;
Notice how not only the font size changes, but so does the spacing and the border radius on the quotation.
You can leave this change un-commented, or comment it back out, which ever you prefer.
Experiment 2 — Increasing the Spacing
Next, let’s make the page more airy without (further) altering the font sizes by doubling the spacer. Uncomment the following line:
--space-rem: 2;
Notice the font sizes have remained the same, but all the paddings and margins have grown.
Again, you can leave this change un-commented, or comment it back out, which ever you prefer.
Experiment 3 — Changing the Quotation Border
Now, let’s make the border around the quotation more subtle by thinning it to a single pixel, adding it to all four sides, making it dashed, reducing the rounding, and rounding all four corners. Uncomment the following three lines:
--quote-border-width: 1px;
--quote-border-style: dashed;
--quote-border-radius: 5px;
Again, you can leave this change un-commented, or comment it back out, which ever you prefer.
Experiment 4 — Changing the Accent Colour
Finally, let’s update the accent colour from the dark blue in the PBS logo to the green in the Let’s Talk Podcasts logo. Uncomment the following line:
--accent-color: #93c01f;
Notice that not only did the heading and quotation text change colour, so did the quotation’s background and border. This is because both of those colours are derived from the accent colour.
Tips & Resources
When developing with CSS custom properties it’s useful to notice that the developer tools in modern browsers show custom property values like they do any other CSS property values, but they may collect them together under a separate heading, and they may label them somewhat inaccurately as ‘Variables’.
In Safari, you can see the value of all defined custom properties by opening the inspector and clicking on an element. In the styles area, select the Computed tab and notice that underneath the diagram of the element’s box model is a collapsible list with the values for all the regular CSS properties on the element, and below that, a collapsed list with the values for all the defined Variables.
In Firefox, similar to Safari, you can select an element in the Inspector tab and then you can see both the standard and custom CSS properties unified into a single list in the Computed tab.
At first glance it appears Google Chrome doesn’t show the custom properties at all, but that’s not quite correct. The custom properties are not displayed anywhere in the Elements tab by default, but they can be enabled by checking the Show all tickbox in the Computed tab’s filter bar located just under the box model diagram. Microsoft Edge, and presumably all the other Chromium browsers, behave the same as Google Chrome.
Personally, I find the Safari Developer tools to be the most powerful and easy to use, so I choose to do my CSS development and testing in that browser.
Useful Links
- The Mozilla Developer Network (MDN) Custom Properties overview — developer.mozilla.org/…
- The MDN
@propertydocs — developer.mozilla.org/… - The MDN docs on the
syntaxdirective within@propertydeclarations — developer.mozilla.org/…
An Optional Challenge — Style a Web App with Custom Properties
If you’d like an opportunity to blow the proverbial dust off your HTML & CSS skills, and to expand them with custom properties, I’ve built a simply little web app that’s ready to be styled. I’ll explain the actual challenge in a moment, but let’s look at the starting point first.
If you open pbs182-challenge-startingPoint.html from the Instalment ZIP in your browser you’ll see a functional but un-styled little calculator widget.
If you open the file in your favourite editor you’ll see the markup for the calculator is pretty simple — a table with:
- A table header with a one-row full-width title cell
- A table body containing the calculator user interface spread over six rows:
- The first row has a single full-width cell with a full-width disabled text input field acting as the calculator’s display
- The other five rows contain the calculator buttons
To facilitate the styling, the HTML is marked up as follows:
- The entire widget is contained in a
<table>with the ID#calculator - The widget’s heading is contained in the table’s
<thead>. - The entire calculator UI is contained in the table’s
<tbody>. - The display text box is an
<input type="text">with the class.display, and it’s contained in a<td>with the class.display-cell - The buttons are arranged in cells (
<td>s) with nothing but a button (<button>) within them, and both the cells and buttons have classes to classify them as being one of three types of button: - The ten digits and the decimal point buttons have the class
.digitand their cells have the class.number-cell - The four arithmetic operator buttons have the class
.operatorand their cells have the class.operator-cell - The Clear (AC), Backspace (⌫), and Calculate (=) buttons have the class
.actionand their cells have the class.action-cell - The one cell with no button has the class
.blank-cell
Looking at the style sheet in the <head> section, you’ll find the basic styles defining the widget’s structure are already present:
/*
* Basic layout — OK TO EDIT (probably not necessary)
*/
table#calculator {
border-collapse: separate;
text-align: center;
}
table#calculator input.display{
text-align: right;
font-family: monospace;
box-sizing: border-box;
width: 100%;
}
table#calculator button {
width: 100%;
}
You shouldn’t need to edit these style declarations, but feel free to edit them if it makes your style sheet simpler or better in some way.
For the most part, these styles should be self explanatory, but I do want to draw your attention to two less common but very important style declarations.
Firstly — the table’s borders have been intentionally un-collapsed (border-collapse: separate;). That is to say, the table, row, and cell borders are rendered separately. This works very poorly when you want a grid of lines like you would get on a spreadsheet (you want border-collapse: collapsed; for that), but you can’t round the table’s outside border when the borders are collapsed. So, because we’re not trying to emulate a spreadsheet, and because we do want to be able to give the widget a nice outside border, I’ve chosen to un-collapse the borders.
Secondly, to avoid margins and paddings being included as part of the display textbox’s width, as would happen by default, the box-sizing is explicitly set to border-box. If you remove the box-sizing: border-box; line then the size: 100% line will cause the text box to be bigger than the space available for it, causing it to break out of its cell!
Your code should replace the following very basic placeholder style:
/* --- YOUR CODE: START --- */
/* Placeholder — REPLACE */
table {
width: 200px;
border: 1px solid black;
border-radius: 5px;
margin: 2rem auto;
}
td {
padding: 0.25rem;
}
/* --- YOUR CODE: END --- */
If you plan to use web fonts be sure to import them at the very top of the style sheet. If you choose to do this, replace the following placeholder:
/* * OPTIONAL PLACEHOLDER - IF YOU WISH TO IMPORT FONTS, DO IT HERE! */
You’ll find the JavaScript powering the widget at the bottom of the file. It imports the current latest stable version of jQuery from the jQuery Content Delivery Network (CDN), then encapsulates the entire widget’s functionality inside a jQuery document ready event handler. You should not need to edit Javascript code in any way.
For the Curious — if you’re wondering how the Javascript hangs together, I opted for a very simple design with fully encapsulated code, so absolutely none of the code exists in the global namespace.
To make the code as readable as possible the document ready hander starts by defining jQuery objects to represent the two most important DOM elements as local variables:
// define variables $calculator = $('#calculator'); $display = $('#calculator input[type="text"].display');The code makes heavy use of HTML data attributes to store metadata directly in the DOM (Document Object Model). The containing table has a data attribute named
stateinitialised toemptyvia the HTML withdata-state="empty". jQuery reads and writes data attributes with its.data()function, so this saves the need for creating state variables. The calculator is a simple state machine that starts in theemptystate, then moves to theaccumulatingstate as the user punches in numbers and operators, then to theresultorerrorstate when the user presses the calculate button (labeled as =).Because the three types of buttons each have their own classes it’s easy to attach appropriate event handlers to the buttons.
There’s just one event handler for all the digits (including the decimal point). A data attribute named
digitcaptured the specific digit a button represents, so the same event handler can be used to handle any digit press, it simply ready the value from the data attribute.The operator buttons work in a similar way, using a data attribute named
operatorthat stores the JavaScript arithmetic operator that matches the button’s label.Each of the three action buttons have their own event hander — the clear button simply changes the state to
empty, thebackspacehandler deletes the last character in the display, and the calculate button calls JavaScript’seval()function with the current input inside atryblock, and writes the appropriate output to the display and moves to theresultorerrorstate depending on whether or not theeval()call threw an error.The simplest of the handlers is literally a one-liner for the clear button, and the most complex of the handers is the one to do the calculation, though even that one’s only about twenty lines of code. The handler for the digits is about average, and a nice illustration of how all the handlers work:
// add click handlers to the digit buttons $('button.digit', $calculator).click(function() { // if the calculator is in the accumulating state, add the digit to the display, otherwise, enter the accumulating state and set the display to the digit if (state() === 'accumulating') { $display.val($display.val() + $(this).data('digit')); } else { state('accumulating'); if($(this).data('digit') === '.') { // if the user clicks the decimal point when not accumulating, start with 0. $display.val('0.'); }else{ $display.val($(this).data('digit')); } } });The other handlers are a little more complex because they implement some extra logic to handle edge cases, smoothing out the user experience. These edge cases minimise the errors that are possible, and make the calculator behave similarly to a physical pocket calculator. For example, starting a calculation with an operator will pre-fix the operator with a
0, and pressing any digit or operator when a result is displayed will clear the calculator and start a new calculation.All the event handlers are implemented as anonymous functions, and to keep the handers simple there are a pair of little helper functions implement as anonymous functions saved to local variables — one to quickly access or update the calculator’s state, and one to empty the calculator and start a new calculation:
// define helper functions state = function(newState) { if (newState !== undefined) { $calculator.data('state', newState); } return $calculator.data('state'); }; empty = function() { state('empty'); $display.val('0'); };
Your Challenge
- Decide on a brand look for the overall widget by choosing the following and capturing each in a custom CSS property:
- A brand colour.
- A pretty font for the branding.
- A simple but legible button font for the buttons that pairs well with the brand font.
- Tune the readability of the UI by adjusting the borders, spacings, and font sizes, being sure to use custom CSS properties to make the various relevant values adjustable.
- Enhance the usability of the UI by:
- Choosing an appropriate font, background colour, and text colour for the display textbox to make it look like what it is — capture each of those three things in custom properties.
- Choosing colours for both the button text and background. It’s important that your UI makes a visual distinction between the three types of buttons (number, operator & action). How you choose to do that is up to you, but you should make the specifics of your choice customisable with custom properties.
Final Thoughts
On the one hand, this instalment covers just one basic feature — variable-like functionality in CSS. However, that one feature has really expanded our CSS opportunities! To illustrate that point and capitalise on our new knowledge, we’ll dedicate the next instalment to customising the standard Bootstrap 5 distribution using only CSS ‘variables’. This will give us an understanding of both how much we can tweak Bootstrap without the SASS CSS pre-processor, and, where the edges of that customisability lie. This will give us a good motivation to learn about SASS in general, and then how to use SASS to customise Bootstrap 5 even more deeply.