Random UTF-8 characters

Home

16 Feb 2019 in bloggatsbycsscoding

Styling with CSS variables

This weekend I've messing around with CSS variables. I figured it might be fun to see if I could build a way to style this site using only CSS variables.

What are CSS variables? Well after all these years we finally have a way to define a value and reuse it in other places. Sure SASS and LESS gave us access to something similar, but compile down to CSS. Variables are dynamic in CSS so we can even be redefine at run time. Their syntax is nice an simple.

Quick note: This post is best viewed in a modern browser that supports css variables.

Defining a new CSS variable looks like this:

.some-css-selector {
  --primary-text-color: #666;
}

Then when you want to use that value:

.another-css-selector {
  color: var(--primary-text-color);
}

You can also provide a fallback value in the case the variable is not defined:

.another-css-selector {
  color: var(--primary-text-color, #777);
}

Right now this blog is a mix of pair CSS files and Emotion for component styling. The CSS files are reset.css which is a generic CSS reset file to ensure a standard look and feel between browsers and theme.css which styles things like links, code blocks and inline code.

Updating everything to use CSS variables was ridiculously simple. I worked out the list of all colors used on my blog and their function. I defined all the new values I need as part of the :root element of my document in theme.css:

:root {
  --primary-text-color: #666;
  --secondary-text-color: #777;
  --heading-text-color: #333;
  --link-text-color: #333;
  --inverted-text-color: #f8f8f8;
  --body-bg-color: #fff;
  --highlight-color-dark: #0d6186;
  --highlight-color-light: rgba(13, 97, 134, 0.45);
  --footer-text-color: #b3b3b3;
  --footer-border-color: #ddd;
  --code-background-color: #fbfaf8;
  --code-inline-color: #c25;
  --code-block-color: #000;
  --code-border-color: rgba(0, 0, 0, 0.15);
}

Then all I needed to do was update any usages of these colors in my CSS files and Emotion styled components. Here's what the Tag component now looks like:

const StyledTag = styled.span`
  display: inline-block;
  text-transform: uppercase;
  margin: 0 4px;
  font-size: .65rem;
  transition: all 350ms;
  position: relative;
  top: -1px;

  a {
    color: var(--inverted-text-color);
    text-decoration: none;
    padding: 3px 5px;
    border-radius: .25rem;
    background-color: var(--highlight-color-light);
    transition: all .5s;
    white-space: nowrap;

    &:hover {
      background-color: var(--highlight-color-dark);
    }
  }
`

Now to the viewer this blog it more or less looks the exact same as before, but one of the coolest things about CSS variables is that you can redefine them at runtime. Given my new blog format I can now create much more interactive demos. Something like this:

Theme Switcher

Select theme an existing theme or create your own!

Custom Theme Builder

You can load any theme and browse around my blog. The theme will remain active until you reload the page. If you are wondering how my ThemeChanger control works here's the source code.

themeChanger.js:

This component handles updating the DOM and state when our CSS variables change.

import React, { useState } from 'react'
import styled from '@emotion/styled'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faEdit } from '@fortawesome/free-solid-svg-icons'

import { themes, cssVarList } from './themes'
import ColorEditor from './colorEditor'
import ThemeButton from './themeButton'

const prettyLabelName = label => label.substring(2).replace(/-/g, ' ')

/**
 * Sets new values for all CSS variables the sites theme uses.
 */
const updateCssVars = theme => {
  const body = document.documentElement
  cssVarList.forEach((variable, i) => {
    body.style.setProperty(variable, theme[i])
  })
}

export const ThemeChanger = () => {
  // hook that stores the active theme
  const [activeTheme, setActiveTheme] = useState(themes['Default Theme'])
  // hook that stores the active edit pane
  const [activeEditor, setActiveEditor] = useState(-1)

  const changeTheme = theme => {
    setActiveTheme(theme)
    updateCssVars(theme)
  }

  const toggleEditor = index => {
    setActiveEditor(activeEditor === index ? -1 : index)
  }

  return (
    <Wrapper>
      <h2>Theme Switcher</h2>
      <p>
        Select theme an existing theme or create your own!
      </p>
      <ThemeButton themeName="Dark Theme v1" setTheme={changeTheme} />
      <ThemeButton themeName="Dark Theme v2" setTheme={changeTheme} />
      <ThemeButton themeName="Default Theme" setTheme={changeTheme} />
      <ThemeButton themeName="Green Theme" setTheme={changeTheme} />
      <ThemeButton themeName="Orange Theme" setTheme={changeTheme} />
      <div>
        <h2>Custom Theme Builder</h2>
        {activeTheme.map((setting, i) => (
          <StyleSettingContainer key={i}>
            <StyledLabel>{prettyLabelName(cssVarList[i])}</StyledLabel>
            <StyledInput onClick={() => toggleEditor(i)} readOnly type="text" value={setting} />
            <EditIcon open={i === activeEditor} onClick={() => toggleEditor(i)} icon={faEdit} size="1x" />
            {activeEditor === i ? (
              <ColorEditor
                setting={setting}
                index={i}
                setActiveEditor={setActiveEditor}
                setTheme={changeTheme}
                activeTheme={activeTheme}
              />
            ) : null}
          </StyleSettingContainer>
        ))}
      </div>
    </Wrapper>
  )
}

const StyleSettingContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 6px;
`

const StyledLabel = styled.label`
  text-transform: capitalize;
  width: 200px;
  text-align: right;
  margin-right: 6px;
`

const StyledInput = styled.input`
  width: 200px;
  border-radius: 6px;
  padding: 5px;
  border: 1px solid #bebebe;
`

const EditIcon = styled(FontAwesomeIcon)`
  position: relative;
  left: -25px;
  color: var(${ ({ open }) => open ? '--highlight-color-dark' : '--highlight-color-light' });
  cursor: pointer;

  &:hover {
    color: var(--highlight-color-dark);
  }
`

const Wrapper = styled.div`
  text-align: center;
  padding: 16px 0;
`

themeButton.js:

A button element that loads a pre-made theme.

import React from 'react'
import styled from '@emotion/styled'
import { themes } from './themes'

const ThemeButton = ({ themeName, setTheme }) => {
  const selectedTheme = themes[themeName]
  return (
    <StyledThemeButton
      type="button"
      color={selectedTheme[6]}
      onClick={() => setTheme(selectedTheme)}
      value={themeName}
    />
  )
}

const StyledThemeButton = styled.input`
  background-color: ${ ({ color }) => color };
  border: 1px solid ${ ({ color }) => color };
  color: #f8f8f8;
  margin: 2px 5px;
  height: 32px;
  border-radius: 6px;

  &:hover {
    cursor: pointer;
    color: ${ ({ color }) => color };
    background-color: #f8f8f8;
  }
`

export default ThemeButton

colorEditor.js:

Opens a react-color color picker and handles updating the color.

import React from 'react'
import styled from '@emotion/styled'
import { SketchPicker } from 'react-color'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons'

const handleColorPickerChange = (index, color, activeTheme, setTheme) => {
  // use hex if the color is solid, else use rgba() so we can include the alpha channel.
  const value = color.rgb.a === 1
    ? color.hex
    : `rgba(${ color.rgb.r }, ${ color.rgb.g }, ${ color.rgb.b }, ${ color.rgb.a })`
  const updatedTheme = activeTheme.map((themeValue, i) => {
    if (i !== index) {
      return themeValue
    }
    return value
  })

  setTheme(updatedTheme)
}

const ColorEditor = ({ setting, index, activeTheme, setTheme, setActiveEditor }) => (
  <Wrapper>
    <Editor>
      <CloseButton onClick={() => setActiveEditor(-1)} icon={faTimesCircle} size="1x" />
      <SketchPicker color={setting} onChange={color => handleColorPickerChange(index, color, activeTheme, setTheme)}/>
    </Editor>
  </Wrapper>
)

const Wrapper = styled.div`
  position: relative;
`

const Editor = styled.div`
  position: absolute;
  top: -42px;
  left: 0px;
`

const CloseButton = styled(FontAwesomeIcon)`
  position: relative;
  right: -117px;
  top: 8px;
  z-index: 1;
  color: var(--primary-text-color);
  cursor: pointer;
`

export default ColorEditor

themes.js

The list of pre-made themes and variables used for this sites theme.

export const themes = {
  'Dark Theme v1': [
    '#dedede',
    '#b8b8b8',
    '#fafafa',
    '#dedede',
    '#dedede',
    '#02111D',
    '#6c6ca3',
    'rgb(88, 110, 128, 0.45)',
    '#dedede',
    '#dedede',
    '#142534',
    '#6c6ca3',
    '##f8f8f8',
    'rgba(255, 255, 255, 0.15)'
  ],
  'Default Theme': [
    '#666',
    '#777',
    '#333',
    '#333',
    '#f8f8f8',
    '#fff',
    '#0d6186',
    'rgba(13, 97, 134, 0.45)',
    '#b3b3b3',
    '#ddd',
    '#fbfaf8',
    '#c25',
    '#000',
    'rgba(0, 0, 0, 0.15)'
  ],
  'Dark Theme v2': [
    '#dedede',
    '#b8b8b8',
    '#fafafa',
    '#dedede',
    '#dedede',
    '#02111D',
    '#ff3366',
    'rgb(255,51,102, 0.45)',
    '#b3b3b3',
    '#ddd',
    '#142534',
    '#ff3366',
    '#f8f8f8',
    'rgba(255, 255, 255, 0.15)'
  ],
  'Green Theme': [
    '#666',
    '#777',
    '#333',
    '#333',
    '#f8f8f8',
    '#fff',
    '#038c36',
    'rgb(3, 140, 54, 0.45)',
    '#b3b3b3',
    '#ddd',
    '#fbfaf8',
    '#038c36',
    '#000',
    'rgba(0, 0, 0, 0.15)',
  ],
  'Orange Theme': [
    '#666',
    '#777',
    '#333',
    '#333',
    '#f8f8f8',
    '#fff',
    '#ff9900',
    'rgb(255, 127, 0, 0.45)',
    '#b3b3b3',
    '#ddd',
    '#fbfaf8',
    '#ff9900',
    '#000',
    'rgba(0, 0, 0, 0.15)',
  ]
}

export const cssVarList = [
  '--primary-text-color',
  '--secondary-text-color',
  '--heading-text-color',
  '--link-text-color',
  '--inverted-text-color',
  '--body-bg-color',
  '--highlight-color-dark',
  '--highlight-color-light',
  '--footer-text-color',
  '--footer-border-color',
  '--code-background-color',
  '--code-inline-color',
  '--code-block-color',
  '--code-border-color'
]

There you have it, my blog can now be easily themed via CSS variables. If you have any questions or comments let me know below. Happy coding!