Using CSS custom properties with vanilla web components is a simple and effective way to enable users with fine-tuned control over presentational aspects of your website.

This tutorial assumes you are familiar with how to [set up a web component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) and implement the [`handleEvent` method](https://webreflection.medium.com/js-class-fields-potentially-harmful-62d68f1a05f3) for event listeners.

Try the working demo to right →

[Try the working demo ↓](/content/articles/web-components-plus-css-custom-properties/#window-example/index.html)

## Setup

If you haven’t visited this page directly, I’m sure you’ve noticed that the site’s colour scheme has changed somewhat dramatically. This was easily done by appending a CSS file for to the page that overwrites the `--theme-primary` and `--theme-secondary` custom properties declared in `main.css`.

```css
:root {
  --theme-primary-hue: 0deg;
  --theme-primary-saturation: 0%;
  --theme-primary-lightness: 0%;
  --theme-secondary-hue: 0deg;
  --theme-secondary-saturation: 100%;
  --theme-secondary-lightness: 100%;
}

body {
  --theme-primary: hsl(
    var(--theme-primary-hue, 0deg),
    var(--theme-primary-saturation, 0%),
    var(--theme-primary-lightness, 0%)
  );
  --theme-secondary: hsl(
    var(--theme-secondary-hue, 0deg),
    var(--theme-secondary-saturation, 0%),
    var(--theme-secondary-lightness, 100%)
  );
}
```

Since these are **C** ascading **S** tyle **S** heets I am overriding the theme props a node lower at the `body` element and added some new granular properties for the theme’s `primary` and `secondary` HSL colour values, which allows us to change each value individually.

The HTML is pretty simple, just my `hsl-slider` component in a `fieldset`. The `hsl-slider` component has a single `color` attribute, which is just used to compose the lookup key for the CSS values above.

```html
<fieldset>
	<legend>Primary Theme Colour</legend>
	<hsl-slider color="primary"></hsl-slider>
</fieldset>
```

## Adding Interactivity

This is where CSS properties in tandem with a web components really shine. First I’ll create const constant values for each slider I want to render, which is just an object containing the name and the max value of the slider (have to differentiate the max hue value from the others).

```javascript
const colorValues = [
  { name: "hue", max: 360 },
  { name: "saturation", max: 100 },
  { name: "lightness", max: 100 },
];
```

Note that I am not explicitly setting values here, since I am opting to get the current values from the CSS instead. Alternatively, I could assign the default values as attributes on my component if I wanted to compose the colour scheme via HTML instead.

In the component `constructor` I can iterate over these objects and look up the corresponding CSS property values from the `:root` selector. I am assuming that the selector is declared as `--theme-{colour}-{name}`, e.g. `--theme-primary-h`.

```javascript
const color = this.getAttribute("color");
const style = getComputedStyle(document.documentElement);

if (!color) {
	return;
}

colorValues.forEach((cv) => {
	const propName = `--theme-${color}-${cv.name}`;
	const cssProp = style.getPropertyValue(propName);
	// Basic sanitation to coerce the value into a number
	const cssPropValue = parseInt(cssProp.trim());
	// Values can either be in % or deg units, so I'll infer by the max
	const isDegreeUnit = cv.max > 100;
	const unit = isDegreeUnit ? "deg" : "%";

// Render logic next
});
```

Since I could adapt this component to render any colour scheme in the future, I’ll render the controls programatically.

```javascript
// Render logic
// First creating all the DOM nodes
const labelEl = document.createElement("label");
// This is a UI-specific component to this site, ignore for this tutorial
const osRangeEl = document.createElement("os-range");
const inputEl = document.createElement("input");
const amountEl = document.createElement("div");

// Setting up some important values on the range slider
inputEl.type = "range";
inputEl.setAttribute("name", cv.name);
inputEl.setAttribute("data-property", propName);
inputEl.setAttribute("data-unit", unit);
inputEl.setAttribute("max", cv.max ?? 100);
// Simple validation, then assign the default value from my CSS property lookup
if (!isNaN(cssPropValue)) {
	inputEl.value = cssPropValue;
}
osRangeEl.appendChild(inputEl);
amountEl.textContent = `${cssPropValue}${unit}`;

labelEl.textContent = cv.name.slice(0, 1);
labelEl.appendChild(osRangeEl);
labelEl.appendChild(amountEl);

this.appendChild(labelEl);
```

This renders me 3 `input[type=range]` sliders, each `value` set to the CSS custom property value from my stylesheet.

The event handler simply gets the unit and property, which has been stored as `--theme-primary-h` and updates the style on the `:root`. Additionally I’ve updated the text so users can see the exact value they’ve selected.

```javascript
handleChange(event) {
	const unit = event.target.getAttribute("data-unit");
	const key = event.target.getAttribute("data-property");
	const value = event.target.value + unit;

event.target.parentElement.nextSibling.textContent = value;
	document.documentElement.style.setProperty(key, value);
}
```

## Finished Code

```js
const colorValues = [
  { name: "hue", max: 360 },
  { name: "saturation", max: 100 },
  { name: "lightness", max: 100 },
];

class HslSlider extends HTMLElement {
  constructor() {
    super();
    const color = this.getAttribute("color");
    const style = getComputedStyle(document.documentElement);

if (!color) {
      return;
    }

// Render logic
      const labelEl = document.createElement("label");
      const osRangeEl = document.createElement("os-range");
      const inputEl = document.createElement("input");
		const amountEl = document.createElement("div");
      inputEl.type = "range";
      inputEl.setAttribute("name", cv.name);
      inputEl.setAttribute("data-property", propName);
      inputEl.setAttribute("data-unit", unit);
      inputEl.setAttribute("max", cv.max ?? 100);
      // Assign the default value
      if (!isNaN(cssPropValue)) {
        inputEl.value = cssPropValue;
      }
      osRangeEl.appendChild(inputEl);
      amountEl.textContent = `${cssPropValue}${unit}`;

labelEl.textContent = cv.name.slice(0, 1);
      labelEl.appendChild(osRangeEl);
      labelEl.appendChild(amountEl);

this.appendChild(labelEl);
    });

this.addEventListener("input", this);
	}

handleEvent(event) {
		if (event.type === "input") {
			this.handleChange(event);
		}
	}

handleChange(event) {
		const unit = event.target.getAttribute("data-unit");
		const key = event.target.getAttribute("data-property");
		const value = event.target.value + unit;

event.target.parentElement.nextSibling.textContent = value;
		document.documentElement.style.setProperty(key, value);
	}
}

customElements.define("hsl-slider", HslSlider);
```

## Next Steps

Now I have a simple HSL slider component that can modify my site’s theme in realtime with just 65 lines of JS and a few CSS custom properties. This is a great first draft, but it could be expanded to include a few more controls like:

- reset all values to default
- additional colour schemes like HEX, RGBA, HWB or LCH
- cache overrides in local or session storage

Admittedly, this is also a more “framework” approach to writing components, where the component is represented as a single DOM node that internally declares the markup for you, so if JavaScript is disabled the UI won’t render at all. Next iteration I can demonstrate how to approach this same feature in a modern, web component way. I’ll definitely be incorporating this into my little `control-panel` component.

#### Tags

- [JavaScript](/content/tags/javascript/index.html)
- [CSS](/content/tags/css/index.html)
- [Web Components](/content/tags/web-components/index.html)

Adjust the sliders to change the website theme

Primary Colourh
145deg
s
50%
l
50%
Secondary Colourh
233deg
s
60%
l
24%
