Converting an object of colors to CSS Custom Properties
CSS Custom Properties
CSS Custom Properties or as they colloquially know, CSS variables, are a powerful new way to expose global values onto your document which can be consumed by multiple styles. While they have fair enough support to justify using it on any modern web application and while there are polyfills for them, they can easily fall back to a previously defined value thanks to the way css is interpreted (line by line).
They tend to look a little like this:
:root {
--blue-primary: #00487C;
--blue-secondary: #027BCE;
}
.my-fun-class {
background-color: var(--blue-primary);
border-color: var(--blue-secondary, #0000FF);
}
In the example above we are declaring two new custom properties on the root. This class matches the root element of a tree representing the document. There after we are using these declared variables in two ways.
background-color: var(--blue-primary);
This line assigns the value of --blue-primary
to the background-color
property of the class its being applied to. If --blue-primary
is not defined, it the same as not adding a value and will be marked as an invalid rule.
However, if we are unsure of the variables existence, or it may be created after the styles have had their first render, you are able to assign a fallback value.
border-color: var(--blue-secondary, #0000FF);
Here the border color will either be the value of --blue-secondary
or #0000FF
. The specification dictates that the first value will always take precedence so on the first render of our component, we will see a border color of #0000FF
until the css variable is available, at which point the browser will repaint and apply the variable color.
From this, its easy to see why css custom properties can be such a valuable tool. You are able to theme your application quite easily and still have the confidence that your colors remain on brand and are named and grouped logically. Linked from Lyft design
Javascript colors
For many developers using javascript based frameworks and libraries, colors may exist in an imported json
file or as an easily exported javascript object. Something like this:
export const palette = {
blue: {
primary: '#00487C',
secondary: '#027BCE',
alternate: '#05FFF6',
},
reds: {
...
}
}
Which is then used in JSS like so:
import { palette } from './narina/colors.js';
const styles = {
button: {
border: 'none',
margin: [5, 10],
color: palette.blue.alternate,
'&:before': {
content: (props) => props.icon,
}
},
}
or in your components directly:
import { palette } from './narina/colors.js';
export default (props) => {
return (
<div style={{ color: palette.blue.primary }}>
Bonjour!
<div/>
);
}
However, if you would like to add some vanilla CSS
back into your project, or perhaps you are switching to sass. Maybe you would prefer to export the styles you have already developed as a consumable design system, in the universal styling language of the web.
This problem is a great candidate for CSS custom properties that can be added to the root of your document and consumed by all your component styles (regardless of the language they are written in).
Getting the best of both worlds
We will use the following method to convert your existing colors javascript object and apply them to the webpage.
This is how my colors are structured:
export const palette = {
blue: {
primary: '#00487C',
secondary: '#027BCE',
alternate: {
primary: '#00355B',
secondary: '#5C8AAB',
}
},
reds: {
...
}
}
- I want to create a function that will take the object as an argument and add the css variables to the page.
-
This function needs to do three distinct things:
- It needs to create the appropriate variable name, similar to the current way in which we access the colors
- It needs to expose deeply nested values (consider having
palette.blues.alternative.primary
) - It needs to add these values to the document without dirtying up the DOM (this means no inline styles)
This seems simple, so lets get started with the first requirement:
// Create a function that will take the object as an argument and add the css variables to our page
export const createCustomProps = (colors) => {
// Get the head of our document
const head = document.head || document.getElementsByTagName('head')[0];
// Create a style element
const style = document.createElement('style');
// Append the style element to the head
head.appendChild(style);
// Set the correct type
style.type = 'text/css';
// Create some css
const css = '';
// Append the new css to the style element
style.appendChild(document.createTextNode(css));
}
If you run this code now, in your application or paste it into your browser console, you will see that a new empty style element gets added to your document head.
Now lets tackle the next requirement, getting the names and values from the palette object. Thinking about this, you will see that in order to keep our code DRY, we would prefer not to access each nested level of the object manually. Rather, lets rely on the function to to that. So I will split the logic out into a new method that will be able to call a new instance of itself.
In order to create a string of css, a flat structure to loop through would be simplest. Thus my requirements for this method are:
- Create a helper function that will consume a javascript object as an argument.
-
Add logic for differing behavior based on the value of the key being a string (a color value) or an object (a nested objected of colors)
- If the value is a color, I should add its key as a variable name and associated value to the flat list.
- If the value is another object, I should find the all the keys and values within that object and add them to the flat list. In order to maintain logical naming, I should also include the parents name as part of the nested color names.
const createFlatObject = (obj) => {
// Lets return an array which matches all the keys in the object passed in
return Object.keys(obj).map((key) => {
// For every array item, lets return an object representing the name and the value
return ({
name: `--${key}`,
value: obj[key]
});
});
};
Running the above method you will see that an array of objects is created for each top level color in the palette object. The name is prefixed with --
.
Next I will add some logic to handle instances where the array item is not a string but rather another object with more color values within.
const createFlatObject = (obj, prefix) => {
// Lets return an array which matches all the keys in the object passed in
return Object.keys(obj).map((key) => {
// For every array item, lets return an object representing the name and the value
if (typeof obj[key] === 'object') {
// if the this is an object, then call createFlatObject with these new arguments
return createFlatObject(obj[key], prefix ? `${prefix}-${key}` : key);
} else {
// otherwise return the original key value pair
return ({
name: prefix ? `--${prefix}-${key}` : `-${key}`,
value: obj[key]
});
}
}).flat();
};
The above code adds three major changes, firstly, I am creating an object only when the typeof
the specific item is not an object. I assume the type in this case to be a string.
When the item is an object we I want to create a an array of objects based of that child's keys and values. This logic is the same as what is being executed on the parent palette object, so I call the createFlatObject
function again, passing in the child object and an additional argument of prefix
to keep track of the parent object.
This is the second major change, since this function calls itself, I can modify its behavior for self invoked instances in the else
block. Now the name is --${prefix}-${key}
if a prefix is (truthy)[https://developer.mozilla.org/en-US/docs/Glossary/Truthy] otherwise it will be --${key}
for all other cases.
This gives me the ability to add a prefix onto the parent level as well, so I could have variables with prefixed names such as --theme-cobalt-blues-primary
.
Lastly I use .flat()
at the end so that the resulting array is just one level deep.
Finally I can call the createFlatObject function within the createCustomProps function:
// Create a function that will take the object as an argument and add the css variables to our page
export const createCustomProps = (colors) => {
// Get the head of our document
const head = document.head || document.getElementsByTagName('head')[0];
// Create a style element
const style = document.createElement('style');
// Append the style element to the head
head.appendChild(style);
// Set the correct type
style.type = 'text/css';
// Create some css
let css = ':root {';
createFlatObject(palette).forEach((color, index) => {
// Add onto the css string
// and if we are on the last color, close the css block,
// otherwise append a new line
css += `${color.name}: ${color.value}; ${index === colors.length - 1 ? '}' : '\n' }`;
});
// Append the new css to the style element
style.appendChild(document.createTextNode(css));
}
The final script should look something like this:
import { palette } from './narina/colors.js';
const createFlatObject = (obj, prefix) => {
return Object.keys(obj).map((key) => {
if (typeof obj[key] === 'object') {
return createFlatObject(obj[key], prefix ? `${prefix}-${key}` : key);
} else {
return ({
name: prefix ? `--${prefix}-${key}` : `-${key}`,
value: obj[key]
});
}
}).flat();
const createCustomProps = (colors) => {
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
head.appendChild(style);
style.type = 'text/css';
let css = ':root {';
createFlatObject(colors).forEach((color, index) => {
css += `${color.name}: ${color.value}; ${index === colors.length - 1 ? '}' : '\n' }`;
});
style.appendChild(document.createTextNode(css));
}
createCustomProps(palette);
Now you can use the custom properties in JSS like this:
// Import not needed!
const styles = {
button: {
border: 'none',
margin: [5, 10],
color: 'var(--blue-primary)',
'&:before': {
content: (props) => props.icon,
}
},
}
or directly in your components like so:
// Import not needed!
export default (props) => {
return (
<div style={{ color: 'var(--blue-primary)' }}>
Bonjour!
<div/>
);
}
I hope this encourages you to use css custom properties in your future projects and helps you finds a good path to migrating your existing style constants to to css.
I would love you hear your feedback on this approach to styling or what other uses you have found of css variables.
Cheers!
-- Maneesh Chiba