Migrate from Tailwind CSS to Web Components with Declarative Shadow DOM
Tags: tailwindcss, dsd, webcomponents, frontend
Published: 2023-07-31 03:34:51

My personal website [https://heybran.cn](https://heybran.cn) was built with Astro and Tailwind CSS. And, since [Declarative Shadow DOM](https://developer.chrome.com/articles/declarative-shadow-dom) has good browser support (except Firefox as the time of writing), I plan to migrate my site from Tailwind CSS to Web Components with Declarative Shadow DOM.

> My site might have been completely migrated from Tailwind CSS to Web Components depending on when you see this post.

I will walk you through how I migrate a single component, and the rest would be repeating the same process.

This is Astro components of skill list on home page. For simplicity, I only show two skills in the codes. **I'm actually screaming at myself: "why I didn't extract out the `li` element as a standalone Astro component?" Guess I was too lazy -- it rarely happened, but hey, human are lazy sometimes -- but now it doesn't matter as it will be replaced with web components ~~soon~~ (already done).**

```html
---
import WebComponents from "@icons/web-components.svg";
import Node from "@icons/node.svg";
---
<ul class="grid grid-cols-1 gap-4 sm:grid-cols-3">
  <li class="flex items-center gap-4">
    <span
      class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
    >
      <img src={WebComponents} class="w-7" alt="Web Components Logo" />
    </span>
    <p class="text-zinc-400">Web Components</p>
  </li>
  <li class="flex items-center gap-4">
    <span
      class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
    >
      <img src={Node} class="w-7" alt="Node.js Logo" />
    </span>
    <p class="text-zinc-400">Node.js</p>
  </li>
</ul>
```

To use Web Components, I creat a `WCSkill.astro` Astro component (prefix `WC` is to differ from other standard Astro components) and then import it to be used by above `Skills.astro` component and relace existing `li` element with `<ASkill>` and pass down `icon` and `name` props which are needed by this component.

```html
---
import WebComponents from "@icons/web-components.svg";
import Node from "@icons/node.svg";
import ASkill from "@components/WCSkill.astro";
---
<ul class="grid grid-cols-1 gap-4 sm:grid-cols-3">
 <ASkill icon={WebComponents} name="Web Components" />
 <ASkill icon={Node} name="Node.js" />
</ul>
```

Next step is to create `WCSkill.astro` component and copy the `li` element down to this component. And update markup with attributes passed down from parent component.

```html
---
const { icon, name } = Astro.props;
---
<li class="flex items-center gap-4">
  <span
    class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
  >
    <img src={icon} class="w-7" alt={name + " Logo"} />
  </span>
  <p class="text-zinc-400">{name}</p>
</li>
```

Now we can bring in Web Components, I wrap existing HTML with a `a-skill` custom element. Up till now nothing is broken in frontend as this custom element will be ingored by browser.

```html
---
const { icon, name } = Astro.props;
---
<a-skill>
  <li class="flex items-center gap-4">
    <span
      class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
    >
      <img src={icon} class="w-7" alt={name + " Logo"} />
    </span>
    <p class="text-zinc-400">{name}</p>
  </li>
</a-skill>
```

Moving on, we need to wrap `li` element with a `template` element with an attribute `shadowrootmode="open"`. A `template` element with the `shadowrootmode` attribute is detected by the browser HTML parser and immediately applied as the shadow root of its parent element.

```html
---
const { icon, name } = Astro.props;
---
<a-skill>
  <template shadowrootmode="open">
    <li class="flex items-center gap-4">
      <span
        class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
      >
        <img src={icon} class="w-7" alt={name + " Logo"} />
      </span>
      <p class="text-zinc-400">{name}</p>
    </li>
  </template>
</a-skill>
```

Now all styles will be broken as our `li` element will live inside `shadowroot` and Tailwind can't reach inside shadowroot. A `style` element comes to the rescue. We will add back all the styles, just using a tag selector would be enough.

```html
---
const { icon, name } = Astro.props;
---
<a-skill>
  <template shadowrootmode="open">
    <style>
      /* your component styles here */
    </style>
    <li class="flex items-center gap-4">
      <span
        class="w-11 h-11 flex items-center justify-center border-t border-zinc-700 bg-zinc-800 rounded-full"
      >
        <img src={icon} class="w-7" alt={name + " Logo"} />
      </span>
      <p class="text-zinc-400">{name}</p>
    </li>
  </template>
</a-skill>
```

Now we can remove the class names  on all our elements. W00t, it looks great, doen't it?

```html
---
const { icon, name } = Astro.props;
---
<a-skill>
  <template shadowrootmode="open">
    <style>
      /* your component styles here */
    </style>
    <li>
      <span>
        <img src={icon} alt={name + " Logo"} />
      </span>
      <p>{name}</p>
    </li>
  </template>
</a-skill>
```

Below are final codes for this component. I don't know about you, but this makes me so damn happy!

```html
---
const { icon, name } = Astro.props;
---
<a-skill>
  <template shadowrootmode="open">
    <style>
      * {
        box-sizing: border-box;
      }
      li {
        display: flex;
        gap: 1rem;
        align-items: center;
      }
      p {
        margin: 0;
      }
      span {
        display: flex;
        height: 2.75rem;
        width: 2.75rem;
        align-items: center;
        justify-content: center;
        border-radius: 50%;
        background: hsl(0, 0%, 100%);
        box-shadow: 0 0 20px 5px rgba(0,0,0,0.05);
      }
      img {
        width: 1.75rem;
        display: block;
        vertical-align: middle;
        max-width: 100%;
        height: auto;
      }
    </style>
    <li>
      <span><img src={icon} alt={name + " Logo"} /></span>
      <p>{name}</p>
    </li>
  </template>
</a-skill>
```