Theme Switcher using CSS Variables and JavaScript

Theme Switcher using CSS Variables and JavaScript
Project: Variable Themes
Author: Ryan Parag
Edit Online: View on CodePen
License: MIT

This code implements a theme switcher using CSS variables and JavaScript. The theme switcher allows users to switch between different themes, such as dark, sunset, sunrise, and light. Each theme is defined by a set of CSS variables that control the colors used in the user interface.

The code dynamically changes the CSS variables on the root element based on the selected theme. It also updates the active state of the theme buttons and adjusts the position of the theme grid based on the selected theme.

The theme switcher provides an intuitive way for users to switch between different visual styles and customize the look and feel of the website or application. By using CSS variables, the code offers flexibility and easy maintenance, as the themes can be easily modified by changing the CSS variable values.

How to Create Theme Switcher using CSS Variables and JavaScript

First of all, load the Google Fonts by adding the following CDN links into the head tag of your HTML document. (Optional)

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;700&display=swap" rel="stylesheet">

Create the HTML structure for theme swichter as follows:

<div class="c-card">
  <button class="c-theme" id="themePicker"></button>
  <h1 class="c-card__title">Variable Themes</h1>
  <p class="c-card__description">Cycle through 4 themes, from darkest to lightest.</p>
  <div class="c-theme-grid" id="themeGrid"></div><a class="c-button" id="button">Cycle Theme</a>
</div>

Style the switcher interface using the following CSS styles:

@charset "UTF-8";
:root {
  --dark-bg: #101214;
  --dark-border: #22272B;
  --dark-surface: #161A1D;
  --dark-text-primary: #DEE4EA;
  --dark-text-secondary: #738496;
  --dark-primary: #1D7AFC;
  --dark-text-inverse: #FFFFFF;
  --sunset-bg: #151c19;
  --sunset-border: #424f4a;
  --sunset-surface: #2f3834;
  --sunset-text-primary: #ecd2c5;
  --sunset-text-secondary: #C0AB92;
  --sunset-primary: #C0AB92;
  --sunset-text-inverse: #151c19;
  --sunrise-bg: #ecd2c5;
  --sunrise-border: #d7c9c6;
  --sunrise-surface: #f3e8e5;
  --sunrise-text-primary: #4f2733;
  --sunrise-text-secondary: #685844;
  --sunrise-primary: #a04d66;
  --sunrise-text-inverse: #f3e8e5;
  --light-bg: #F7F8F9;
  --light-border: #F1F2F4;
  --light-surface: #FFFFFF;
  --light-text-primary: #091E42;
  --light-text-secondary: #626F86;
  --light-primary: #1D7AFC;
  --light-text-inverse: #FFFFFF;
  --bg: var(--dark-bg);
  --border: var(--dark-border);
  --surface: var(--dark-surface);
  --text-primary: var(--dark-text-primary);
  --text-secondary: var(--dark-text-secondary);
  --primary: var(--dark-primary);
  --text-inverse: var(--dark-text-inverse);
}

*, *:before, *:after {
  box-sizing: border-box;
}

html {
  box-sizing: inherit;
  font-size: 62.5%;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

body {
  font-size: 1.6rem;
  font-family: "Figtree", system-ui, sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.2rem;
  background: var(--bg);
  color: var(--text-secondary);
}
.cd__main{
  max-width: 1280px !important;
}
.c-card {
  width: 100%;
  max-width: 650px;
  border: 1px solid var(--border);
  border-radius: 1.6rem;
  padding: 3.2rem;
  background: var(--surface);
  position: relative;
  box-shadow: 0.3px 0.5px 0.7px rgba(0, 0, 0, 0.08), 0.8px 1.6px 2px -0.8px rgba(0, 0, 0, 0.08), 2.1px 4.1px 5.2px -1.7px rgba(0, 0, 0, 0.08), 5px 10px 12.6px -2.5px rgba(0, 0, 0, 0.08);
}
.c-card__title {
  margin: 0 0 0.8rem;
  padding: 0;
  line-height: 1.2;
  font-size: 4rem;
  color: var(--text-primary);
}
.c-card__description {
  margin: 0;
  padding: 0;
  line-height: 150%;
  font-size: 2rem;
  color: var(--text-secondary);
}

.c-button {
  display: inline-flex;
  padding: 1.2rem 2rem;
  background: var(--primary);
  border-radius: 0.8rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text-inverse);
  font-weight: 700;
  user-select: none;
  position: relative;
  transition: all 120ms ease-out;
}
.c-button:hover, .c-button:focus {
  outline: none;
  transform: scale(1.03);
}

.c-theme {
  position: absolute;
  top: 2.4rem;
  right: 2.4rem;
  width: 4rem;
  height: 4rem;
  cursor: pointer;
  display: inline-block;
  overflow: hidden;
  padding: 0;
  margin: 0;
  background: transparent;
  color: var(--text-primary);
  border: 1px solid transparent;
  border-radius: 0.8rem;
  padding: 0.4rem;
  transition: all 120ms ease-out;
}
.c-theme:hover, .c-theme:focus {
  border-color: var(--border);
}
.c-theme:after, .c-theme:before {
  content: "";
  position: absolute;
  z-index: 10;
}
.c-theme:after {
  top: 0;
  left: 0;
  right: 0;
  height: 0.8rem;
  background: linear-gradient(to bottom, var(--surface), transparent);
}
.c-theme:before {
  bottom: 0;
  left: 0;
  right: 0;
  height: 0.8rem;
  background: linear-gradient(to top, var(--surface), transparent);
}
.c-theme:focus {
  outline: none;
}
.c-theme__grid {
  position: relative;
  width: 3.2rem;
  transition: all 240ms ease-out;
}
.c-theme svg {
  width: 3.2rem;
  height: 3.2rem;
}
.c-theme svg:focus {
  outline: none;
}

.c-box {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text-secondary);
  position: relative;
  padding: 1.6rem;
  border-radius: 1.2rem;
  border: 1px solid var(--border);
  user-select: none;
  cursor: pointer;
  transition: all 120ms ease-out;
}
.c-box:hover, .c-box:focus {
  transform: scale(1.03);
}
.c-box__title {
  display: flex;
  align-items: center;
  width: 100%;
}
.c-box__icon {
  width: 1.6rem;
  height: 1.6rem;
  margin-right: 0.4rem;
}
.c-box__swatches {
  display: flex;
  flex-wrap: wrap;
  margin-top: 0.8rem;
}
.c-box--active {
  outline: 4px solid var(--primary);
}
.c-box--active:after {
  content: "✓";
  position: absolute;
  top: -1.2rem;
  right: -1.2rem;
  height: 2.4rem;
  width: 2.4rem;
  background: var(--primary);
  border-radius: 999px;
  color: var(--text-inverse);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.c-swatch {
  width: 2rem;
  height: 2rem;
  display: inline-block;
  border-radius: 999px;
  border: 1px solid var(--border);
  margin-right: -0.8rem;
  box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.12), 0px 0px 0px 1px rgba(0, 0, 0, 0.08);
}

.c-theme-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 1.6rem;
  margin: 3.2rem 0;
}
@media (max-width: 700px) {
  .c-theme-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

Finally, add the following JavaScript function to activate the theme switcher.

const themes = ['dark', 'sunset', 'sunrise', 'light'];
let count = 0;
const themePicker = document.getElementById('themePicker');
const themeList = document.getElementById('themeGrid');

// Change the CSS variables on the root element, depending on the curent theme
const changeTheme = (theme) => {
	
	if(count < 3) {
		count += 1;
	} else {
		count = 0
	}
	
	document.documentElement.style.setProperty('--bg', `var(--${theme}-bg)`);
	document.documentElement.style.setProperty('--border', `var(--${theme}-border)`);
	document.documentElement.style.setProperty('--surface', `var(--${theme}-surface)`);
	document.documentElement.style.setProperty('--text-primary', `var(--${theme}-text-primary)`);
	document.documentElement.style.setProperty('--text-secondary', `var(--${theme}-text-secondary)`);
	document.documentElement.style.setProperty('--primary', `var(--${theme}-primary)`);
	document.documentElement.style.setProperty('--text-inverse', `var(--${theme}-text-inverse)`);
	
	const themeGrid = themePicker.querySelector('.c-theme__grid')
	
	if(themeList.querySelector('.c-box--active')) {
		themeList.querySelector('.c-box--active').classList.remove('c-box--active')
	}
	
	themeList.querySelectorAll('.c-box').forEach(item => {
		if(item.dataset.theme === theme) {
			item.classList.add('c-box--active')
		}
	})
	
	switch(theme) {
		case theme = 'dark':
			themeGrid.style.top = '0'
			break;
		case theme = 'sunset':
			themeGrid.style.top = '-3.6rem'
			break;
		case theme = 'sunrise':
			themeGrid.style.top = '-7.1rem'
			break;
		case theme = 'light':
			themeGrid.style.top = '-10.7rem'
			break;
	}
}

// Define Icons
const darkIcon = `<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24" tabindex="-1" title="Dark"><path d="M10 2c-1.82 0-3.53.5-5 1.35C7.99 5.08 10 8.3 10 12s-2.01 6.92-5 8.65C6.47 21.5 8.18 22 10 22c5.52 0 10-4.48 10-10S15.52 2 10 2z"></path></svg>`

const sunsetIcon = `<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24" tabindex="-1" title="Sunset"><path d="M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-.89 0-1.74-.2-2.5-.55C11.56 16.5 13 14.42 13 12s-1.44-4.5-3.5-5.45C10.26 6.2 11.11 6 12 6c3.31 0 6 2.69 6 6s-2.69 6-6 6z"></path></svg>`

const sunriseIcon = `<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24" tabindex="-1" title="Sunrise"><path d="M20 15.31 23.31 12 20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69zM12 18V6c3.31 0 6 2.69 6 6s-2.69 6-6 6z"></path></svg>`

const lightIcon = `<svg fill="currentColor" aria-hidden="true" viewBox="0 0 24 24" tabindex="-1" title="Light"><path d="M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"></path></svg>`

// Content and click function for the theme picker
themePicker.innerHTML = `
	<div class="c-theme__grid">
		${darkIcon}
		${sunsetIcon}
		${sunriseIcon}
		${lightIcon}
	</div>
`

themePicker.onclick = () => {
	changeTheme(themes[count])
}

document.getElementById('button').onclick = () => {
	changeTheme(themes[count])
}

const capitalized = (word) => {
	return word.charAt(0).toUpperCase() + word.slice(1)
}

themes.forEach((theme, i) => {
	let box = document.createElement('button');
	box.dataset.theme = theme
	box.onclick = () => {
		changeTheme(themes[i])
	}
	box.classList = 'c-box';
	box.style.setProperty('--bg', `var(--${theme}-bg)`);
	box.style.setProperty('--border', `var(--${theme}-border)`);
	box.style.setProperty('--surface', `var(--${theme}-surface)`);
	box.style.setProperty('--text-primary', `var(--${theme}-text-primary)`);
	box.style.setProperty('--text-secondary', `var(--${theme}-text-secondary)`);
	box.style.setProperty('--primary', `var(--${theme}-primary)`);
	box.style.setProperty('--text-inverse', `var(--${theme}-text-inverse)`);
	
	const iconRender = (theme) => {
		switch(theme) {
			case theme = 'dark':
				return darkIcon
				break;
			case theme = 'sunset':
				return sunsetIcon
				break;
			case theme = 'sunrise':
				return sunriseIcon
				break;
			case theme = 'light':
				return lightIcon
				break;
		}
	}
	
	box.innerHTML = `
		<div class="c-box__title">
			<span class="c-box__icon">
				${iconRender(theme)}
			</span>
			<label>${capitalized(theme)}</label>
		</div>
		<div class="c-box__swatches">
			<span class="c-swatch" style="background: var(--bg)" title="bg"></span>
			<span class="c-swatch" style="background: var(--border)" title="border"></span>
			<span class="c-swatch" style="background: var(--surface)" title="surface"></span>
			<span class="c-swatch" style="background: var(--text-primary)" title="text-primary"></span>
			<span class="c-swatch" style="background: var(--text-secondary)" title="text-secondary"></span>
			<span class="c-swatch" style="background: var(--primary)" title="primary"></span>
			<span class="c-swatch" style="background: var(--text-inverse)" title="text-inverse"></span>
		</div>
	`
	themeList.appendChild(box)
})

changeTheme(themes[0])

That’s all! hopefully, you have successfully created theme switcher using CSS variables and JavaScript. If you have any questions or suggestions, feel free to comment below.

Show 1 Comment

1 Comment

Leave a Reply

Your email address will not be published. Required fields are marked *