In my [first article](/content/articles/web-components-plus-css-custom-properties/index.html), I made a basic web component that can adjust my site’s 2-colour scheme by reading the custom properties assigned to `:root` and binding them to a ranged input.

For my first pass, I chose to declare the HSL slider UI imperatively.

```javascript
const labelEl = document.createElement("label");
const inputEl = document.createElement("input");
inputEl.type = "range";

labelEl.appendChild(inputEl);
this.appendChild(labelEl);
```

And honestly, there was no specific reason why I chose this method, other than that it’s how I’ve been writing JavaScript-based interfaces (primarily in React) for the past 10 years, HTML is just the delivery mechanism for JavaScript bundles containing UI code. Even worse, my component’s features are very specific; it assumes that a website would want to adjust individual HSL values that have to be present in a CSS file.

But today, we’re going to undertake a brief refactor, where instead of using JavaScript to draw the UI DOM tree, we’ll instead just fallback to good old, reliable HTML and wrap the parts we want to make interactive in our refactored component, which (spoiler alert!) also won’t be restricted to just HSL now.

## The Markup

To start, I’ll insert all those HTML elements I described in the original component as HTML, along with the refactored and renamed `theme-control` component.

```html
<div class="hsl-container">
  <fieldset>
    <legend>Primary Colour</legend>
    <theme-control>
      <label for="primary-h">H</label>
      <os-range>
        <input
          type="range"
          id="primary-h"
          name="--theme-primary-hue"
          max="360"
          data-unit="deg"
        />
      </os-range>
      <div></div>
      <label for="primary-s">S</label>
      <os-range>
        <input
          type="range"
          id="primary-s"
          name="--theme-primary-saturation"
          data-unit="%"
        />
      </os-range>
      <div></div>
      <label for="primary-l">L</label>
      <os-range>
        <input
          type="range"
          id="primary-l"
          name="--theme-primary-lightness"
          data-unit="%"
        />
      </os-range>
      <div></div>
    </theme-control>
  </fieldset>
  <fieldset>
    <legend>Secondary Colour</legend>
    <theme-control>
      <label for="secondary-h">H</label>
      <os-range>
        <input
          type="range"
          id="secondary-h"
          name="--theme-secondary-hue"
          max="360"
          data-unit="deg"
        />
      </os-range>
      <div></div>
      <label for="secondary-s">S</label>
      <os-range>
        <input
          type="range"
          id="secondary-s"
          name="--theme-secondary-saturation"
          data-unit="%"
        />
      </os-range>
      <div></div>
      <label for="secondary-l">L</label>
      <os-range>
        <input
          type="range"
          id="secondary-l"
          name="--theme-secondary-lightness"
          data-unit="%"
        />
      </os-range>
      <div></div>
    </theme-control>
  </fieldset>
</div>
```

A couple of things to note here:

1. Each input’s `name` attribute is bound to the CSS custom property it can mutate.
2. I don’t love it, but each input declares the unit symbol that the updated CSS value will belong to, so I don’t have to parse all the possibilities in JS.
3. I’ve deleted the `color` attribute from the component, since it’s no longer required to build the CSS property name dynamically; they’re all statically assigned by the HTML.

## Updated `constructor`

Now I can refactor the `theme-control`’s `constructor` to simply query its children for all the `input[type=range]`, read the corresponding CSS value bound to the input and assign it as the starting value, as well as the adjacent `div`’s text reflecting the live value.

```javascript
 const style = getComputedStyle(this.stylesSource);
/** @type NodeListOf<HTMLInputElement> */
const inputs = this.querySelectorAll("input[type=range]");

inputs.forEach((inputEl) => {
  /** @type HTMLDivElement */
  const amountEl = inputEl.parentNode.nextElementSibling;
  const cssProp = style.getPropertyValue(inputEl.name);
  const unit = inputEl.getAttribute("data-unit") ?? "";
  // Basic sanitation to coerce the value into a number
  const cssPropValue = parseInt(cssProp.trim());

// Assign the default value
  if (isNaN(cssPropValue)) {
    this.stylesSource.style.setProperty(cssPropValue, inputEl.value + unit);
  } else {
    inputEl.value = cssPropValue;
    // This is optional, used to trigger event listeners for presentational
    // components in Chromium/WebKit
    inputEl.dispatchEvent(
      new Event("input", {
        bubbles: true,
        cancelable: true,
      }),
    );
  }
  amountEl.textContent = cssPropValue + unit;
});

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

There’s a lot less heavy lifting by the component now, it’s just responsible for keeping track of input changes and assigning the default values, but even then if there’s no match it can just defer to the HTML’s assigned value.

## Event Handler

Handling the `input` event works basically the same, though again I’m doing less parsing and internally and instead just deferring to the HTML structure for the values I need.

```javascript
handleChange(event) {
	/** @type HTMLDivElement */
	const amountEl = event.target.parentElement?.nextElementSibling;
	const unit = event.target.getAttribute("data-unit") ?? "";
	const value = event.target.value + unit;

if (amountEl) {
	  amountEl.textContent = value;
	}
	this.stylesSource.style.setProperty(event.target.name, value);
}
```

## Bonus: Theme anything

One other design flaw in my first pass was that the component was limited to only target the `:root`’s CSS properties. But what if the component needed to control a singular parent deeper in the DOM tree? To address this gap I added a `stylesSource` property to the `ThemeControl` class, which defaults to the `document.documentElement`, but if a `source` attribute is assigned to the `theme-control` component it’ll use that DOM node instead.

```javascript
class ThemeControl extends HTMLElement {
  /** @type HTMLElement */
  stylesSource = document.documentElement;

constructor() {
    super();
    if (this.hasAttribute("source") && this.getAttribute("source")) {
      this.stylesSource = this.closest(this.getAttribute("source"));
    }
    // ...
}
```

I’ve added a second dialog to this post that lets you control the RGB of its parent element. Have fun creating anarchy with my site’s theme!

Adjust the sliders to change the website theme

### Primary Colour
- H 145deg
- S 50%
- L 50%

### Secondary Colour
- H 233deg
- S 60%
- L 24%

Now it's really easy to make an RGB slider, and in this case is just scoped to the primary theme colour of this window.

### Primary Colour
- R 255
- G 0
- B 255
