For a lot of initiatives I work on it’s helpful to outline all of our model colors in a JavaScript file, significantly as I work on a whole lot of knowledge visualisations that use them. Right here’s an abridged instance of how I outline model colors, in addition to these used for knowledge visualisations, and their variants:
// theme.js
const theme = {
coloration: {
model: {
major: {
DEFAULT: '#7B1FA2',
mild: '#BA68C8',
darkish: '#4A148C',
},
secondary: {
DEFAULT: '#E91E63',
mild: '#F48FB1',
darkish: '#C2185B',
},
},
knowledge: {
blue: '#40C4FF',
turquoise: '#84FFFF',
mint: '#64FFDA',
},
},
}
I additionally want these variables in my CSS, the place they’re outlined as customized properties. However I don’t need to have to keep up my color theme in two locations! That’s why I created a script to create a CSS file that defines customized properties from a JS supply file. For those who’re , right here’s the way it’s completed.
Setup
For this walkthrough you’ll want Node and NPM put in. For those who’re already acquainted with organising a mission utilizing NPM, you’ll be able to skip over this half. In any other case, assuming you’ve already put in NPM globally, you’ll must run npm init
in your mission root and comply with the prompts. This creates a package deal.json file within the root of your mission listing.
Create a script file
We’ll must create a JS file for our script so we are able to run it from the command line. For simplicity, let’s create a file known as index.js within the mission root, and add a single line:
// index.js
console.log('Good day world')
Now we should always have the ability to run node index.js
from the terminal and see our “Good day world” message, so we all know our very primary script has run efficiently.
Import the theme
Now let’s import the theme outlined within the JS file from which we need to create our CSS customized properties. We’ll name this theme.js. You’ll want to verify your file exports the theme so it may be imported elsewhere.
// theme.js
const theme = {
// Theme colors as outlined above...
}
export default theme
// index.js
import theme from './theme.js'
console.log(theme)
Working the script once more with node index.js
, we should always see the theme object logged within the terminal. Now we have to truly do one thing with it!
Enter and output
The intention right here is to create CSS customized properties that correspond to the theme object keys. For instance:
// theme.js
const theme = {
coloration: {
major: 'purple',
secondary: 'blue',
},
}
Would develop into:
/* kinds.css */
:root {
--color-primary: purple;
--color-secondary: blue;
}
Nevertheless, our theme as outlined in our JS file isn’t fairly so easy. As you’ll be able to see from the instance at first, a few of our color definitions embody a number of lighter or darker variants, nested a couple of degree deep.
What we wish right here is to map our colors in order that their customized property names are prefixed with their ancestor property names. For instance, we might use --color-brand-primary
for the default major model color, and --color-brand-primary-light
for its lighter variant.
:root {
--color-brand-primary: #7b1fa2;
--color-brand-primary-light: #ba68c8;
}
We shouldn’t assume that every one color could have the identical property names both. We must always have the ability to outline them utilizing any names we like, as many ranges as is required.
Notice, I’m together with coloration
right here as a property of theme
. That’s as a result of the precise theme configuration would possibly embody issues like font households too. We’ll preserve it easy and deal with color right here, however the script we’re going to put in writing ought to (theoretically!) work for any object properties of the theme.
Writing a recursive operate
We’ll write a operate that appears at any key/worth pair and returns the CSS customized property definition as a string.
The primary half is straightforward sufficient:
// index.js
import theme from './theme.js'
const mapTheme = ([key, value]) => {
// If worth is a string, return the outcome
if (typeof worth === 'string') {
return `--${key}: ${worth}`
}
}
This could work advantageous is we had a quite simple theme, like this:
const theme = {
purple: '#7B1FA2',
pink: '#E91E63',
}
We may convert our theme object to an array utilizing Object.entries()
and map over the entries with this operate:
// index.js
import theme from './theme.js'
const mapTheme = ([key, value]) => {
// If worth is a string, return the outcome
if (typeof worth === 'string') {
return `--${key}: ${worth}`
}
}
console.log(Object.entries(theme).map(mapTheme))
// outcome: ['--purple: #7B1FA2', '--pink: #E91E63']
Nevertheless, that’s not going to be sufficient for our nested theme variables. As a substitute we’ll amend the mapTheme()
operate in order that if the worth is not a string
//index.js
const mapTheme = ([key, value]) => {
// If worth is a string, return the outcome
if (typeof worth === 'string') {
return `--${key}: ${worth}`
}
// In any other case, name the operate once more to test the following pair
return Object.entries(worth).flatMap(mapTheme)
}
console.log(Object.entries(theme).flatMap(mapTheme))
You would possibly discover we’re utilizing the flatMap()
array methodology as a substitute of map()
as above. That is in order that the result’s output as a flat array, which is what we would like, as a substitute of nesting the customized properties.
If we test the outcome at this level, we’ll see it’s not fairly what we would like. We find yourself with customized property names that correspond to the nested object keys however don’t inform us something concerning the father or mother teams. We additionally find yourself with duplicates:
[
'--DEFAULT: #7B1FA2',
'--light: #BA68C8',
'--dark: #4A148C',
'--DEFAULT: #E91E63',
'--light: #F48FB1',
'--dark: #C2185B',
'--blue: #40C4FF',
'--turquoise: #84FFFF',
'--mint: #64FFDA',
]
If we would like extra helpful customized property names we’ll must append the title to its father or mother group title, except the secret’s DEFAULT
, wherein case we’ll merely return the father or mother group key.
// index.js
import theme from './theme.js'
const mapTheme = ([key, value]) => {
// If worth is a string, return the outcome
if (typeof worth === 'string') {
return `--${key}: ${worth}`
}
return Object.entries(worth).flatMap(([nestedKey, nestedValue]) => {
// Append to the customized property title, except default worth
const newKey = nestedKey === 'DEFAULT' ? key : `${key}-${nestedKey}`
// Verify the brand new key/worth pair
return mapTheme([newKey, nestedValue])
})
}
console.log(Object.entries(theme).flatMap(mapTheme))
This ends in way more useful names:
[
'--color-brand-primary: #7B1FA2',
'--color-brand-primary-light: #BA68C8',
'--color-brand-primary-dark: #4A148C',
'--color-brand-secondary: #E91E63',
'--color-brand-secondary-light: #F48FB1',
'--color-brand-secondary-dark: #C2185B',
'--color-data-blue: #40C4FF',
'--color-data-turquoise: #84FFFF',
'--color-data-mint: #64FFDA',
]
Another with a for
loop
By the way in which, we may do that in a barely completely different manner with a for
loop. It’s the same quantity of code, however we don’t want the nested flatMap
, which could make for a barely extra elegant resolution (you be the choose!):
// index.js
let outcome = []
const mapTheme = (obj, key = null) => {
for (const property in obj) {
let title = key || property
if (property !== 'DEFAULT' && !!key) {
title = `${key}-${property}`
}
if (typeof obj[property] === 'string') {
outcome.push(`--${title}: ${obj[property]}`)
} else {
mapTheme(obj[property], title)
}
}
}
mapTheme(theme)
console.log(outcome)
Writing to a file
Now we are able to take these values and write them to a CSS file to be used in our mission. We may merely copy them from the console, however even higher if we write a script that can do it for us.
We’ll import the writeFile
methodology from the Node JS library and write a brand new async operate known as buildTheme
, which we’ll export. (We’ll take away the console log from the earlier instance.)
// index.js
import { writeFile } from 'fs/guarantees'
import theme from './theme.js'
const mapTheme = ([key, value]) => {
/* ... */
}
const buildTheme = async () => {
attempt {
console.log(Object.entries(theme).flatMap(mapTheme))
} catch (e) {
console.error(e)
}
}
buildTheme()
We must always now have the ability to run the script from the command line by typing node index.js
and see the outcome logged.
Subsequent we’ll convert the customized properties into an acceptable format for our CSS file. We’ll need every customized property to be indented and set by itself line, which we are able to do with the escaped characters n
and t
respectively.
// index.js
import { writeFile } from 'fs/guarantees'
import theme from './theme.js'
const mapTheme = ([key, value]) => {
/* ... */
}
const buildTheme = async () => {
attempt {
const outcome = Object.entries(theme).flatMap(mapTheme)
// Indent every customized property and append a semicolon
let content material = outcome.map((line) => `t${line};`)
// Append and prepend brackets, and put every merchandise on a brand new line
content material = [':root {', ...content, '}'].be a part of('n')
console.log(content material)
} catch (e) {
console.error(e)
}
}
buildTheme()
All that continues to be is to put in writing the outcome to a CSS file, utilizing the writeFile()
methodology. We’ll must specify the situation of the file we need to write to, and its character encoding, which can be 'utf-8'
. We’re together with a useful console log informing the consumer that the file has been written, and guaranteeing we catch any errors by additionally logging them to the console.
// index.js
import { writeFile } from 'fs/guarantees'
import theme from './theme.js'
const mapTheme = ([key, value]) => {
/* ... */
}
const buildTheme = async () => {
attempt {
const outcome = Object.entries(theme).flatMap(mapTheme)
let content material = outcome.map((line) => `t${line};`)
content material = [':root {', ...content, '}'].be a part of('n')
// Write to the file
await writeFile('src/theme.css', content material, { encoding: 'utf-8' })
console.log('CSS file written')
} catch (e) {
console.error(e)
}
}
buildTheme()
Working the script now outputs the CSS file we’d like.
@theme {
--color-brand-primary: #7b1fa2;
--color-brand-primary-light: #ba68c8;
--color-brand-primary-dark: #4a148c;
--color-brand-secondary: #e91e63;
--color-brand-secondary-light: #f48fb1;
--color-brand-secondary-dark: #c2185b;
--color-data-blue: #40c4ff;
--color-data-turquoise: #84ffff;
--color-data-mint: #64ffda;
}
Right here’s the entire file:
// index.js
import { writeFile } from 'fs/guarantees'
import theme from './theme.js'
const mapTheme = ([key, value]) => {
if (typeof worth === 'string') {
return `--${key}: ${worth}`
}
return Object.entries(worth).flatMap(([nestedKey, nestedValue]) => {
const newKey = nestedKey === 'DEFAULT' ? key : `${key}-${nestedKey}`
return mapTheme([newKey, nestedValue])
})
}
const buildTheme = async () => {
attempt {
const outcome = Object.entries(theme).flatMap(mapTheme)
let content material = outcome.map((line) => `t${line};`)
content material = [':root {', ...content, '}'].be a part of('n')
await writeFile('src/theme.css', content material, { encoding: 'utf-8' })
console.log('CSS file written')
} catch (e) {
console.error(e)
}
}
buildTheme()