Migrate from Tailwind CSS to Web Components with Declarative Shadow DOM
My personal website was built with Astro and Tailwind CSS. And, since 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.
This 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).
---
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.
---
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.
---
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.
---
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.
---
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.
---
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?
---
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!
---
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>