Themeable Web Components with Solid.js and CSS lite-dark()

KonstantinKonstantin

I've been following Solid.js for quite a while and even recorded a video series about migrating to Solid.js, so when I found the solid-element project, I got up to speed with it organically.

In this tutorial, we will build a resizable-button web component with support for light/dark theme switching.

Resizable Button Web Component Screenshot

Let's start from the basics.

About Solid.js and solid-element

To put it simply, Solid.js is a React-like framework with improvements, like better reactivity. The solid-element project is an extension for Solid.js that lets you build Web Components using JSX and Solid.js primitives.

The Web Components built with solid-element have no external dependencies, meaning you can use them in static HTML as any other HTML element, like Input or Button. It doesn't matter what framework you're building with (React, Vue, Svelte), the web components are going to work there.

Do you see why many companies bet on Web Components? Right, you build once and for all frameworks.

About light-dark()

The light-dark() CSS function is a modern vanilla CSS light/dark color switching mechanism.

Creating a Web Component

Let's define a custom element called resizable-button with its default size.

// resizable-button.tsx
import { customElement } from 'solid-element'
import { createSignal } from 'solid-js'

export interface ResizableButtonProps {
  defaultSize?: number
}

// ...

customElement(
  'resizable-button',
  { defaultSize: 10 },
  (props: ResizableButtonProps) => {
    const { defaultSize } = props

    const [buttonSize, setButtonSize] = createSignal(defaultSize!)

    const handleButtonClick = async () => {
      setButtonSize((currentSize) => ++currentSize)
    }

    return (
      <div>
        <style>{styles(buttonSize())}</style>
        <button onClick={handleButtonClick}>Click to Enlarge</button>
      </div>
    )
  }
)

Noticed familiar JSX syntax in the component markup? Anything else similar to React? It's how Solid.js defines state variables like buttonSize. It syntactically resembles useState() in React if you know what I mean.

Here, when user clicks the button, the handleButtonClick handler increments the buttonSize variable.

Styling Shadow DOM

In their turn, styles for the component depend on the buttonSize variable value.

const styles = (size: number) => `
:host button {
  /* ... */
  border-radius: ${size * 0.5}px;
  padding: ${size}px ${size * 1.5}px;
  font-size: ${size * 1.5}px;
  /* ... */
}
`

By default solid-element creates components with Shadow DOM enabled, meaning that the styles are isolated there. I'll keep it this way, but if you want to disable this default behavior, you can put noShadowDOM() from solid-element to the component function.

As you may see, I'm using :host selector to refer to the host component element resizable-button in the Shadow DOM environment.

Theming with light-dark()

Next thing I'm going to draw your attention to is the light-dark() color switcher.

const styles = (size: number) => `
:host button {
  /* ... */
  background-color: light-dark(indigo, white);
  color: light-dark(white, indigo);
  border: 1px solid light-dark(white, indigo);
  /* ... */
}
`

In the example above, the background-color: light-dark(indigo, white) line reads the following: "We set the background color of the button to indigo for the light theme and to white for the dark theme."

Finally, you need to add support for the light and dark theme switching to the HTML page.

<!doctype html>
<html lang="en" class="light">
  <head>
    <style>
      :root { color-scheme: light dark; }
      .light { color-scheme: light; }
      .dark { color-scheme: dark; }
    </style>

    <script>
      function toggleTheme() {
        document.documentElement.classList.toggle('light');
        document.documentElement.classList.toggle('dark');
      }
    </script>

    <script type="module" src="/vendor/resizable-button.mjs"></script>
  </head>
  <body>
    <resizable-button />

    <button onClick="toggleTheme()">Toggle Theme</button>
  </body>
</html>

The most important piece here is :root { color-scheme: light dark; } which add support for both color themes (aka schemes).

By toggling light and dark CSS classes for the html element through JavaScript, we change the resizable-button theme here.

I intentionally provided vanilla HTML example to confirm that solid-element components don't require any external dependencies, which allows using them with the framework of our choice.

Check out the project demo and the source on Github.

Potential improvements

While writing the post, I didn't set the goal to make the code perfect, so here are some possible improvements:

  1. Use CSS variables for base colors instead of white and indigo.
  2. Pass background and foreground colors to the resizable-button component through attributes.
  3. For anything but colors, use color scheme media queries instead of light-dark().

That's it for today. Feel free to ping me on X or Linkedin if you have any feedback. Thanks.

Relevant Posts

Get in Touch

If you have an idea or look for a developer for your project, we need to talk.