I always thought I had a good enough idea about Web Components. In my mind, you used them for two specific reasons:
- To use a component from a third-party library in your app.
- To build your own components that needed to be used in other people's apps, maybe built with React or Vue.
It was all about interoperability. I figured that as long as I was happily building my own apps with Svelte and didn't have to share my components with the outside world, I would never need them. The idea of building a Web Component for use inside my own SvelteKit project seemed completely pointless.
And then, a simple Markdown file proved me wrong and I found a cool use case for it.
The Problem: A Smart Button in a Dumb Box
For this very blog, I wanted to add a "Copy" button to all my code snippets. It's a common feature, and it should be simple, right?
My blog posts are written in Markdown. I use a library called marked
to turn my Markdown into HTML. The process looks like this:
Markdown File → marked
Processor → Plain HTML → Rendered on the page by SvelteKit
Here’s the catch: the conversion to HTML happens before Svelte ever sees it. The output is just a string of static HTML. I can't just tell my Markdown processor, "Hey, please render this interactive <SvelteCopyButton />
here." It doesn't know what a Svelte component is. It only knows HTML.
I could try some messy regex to inject the button's HTML directly into the string. Or maybe I could write a separate TypeScript script that runs after the page loads, finds all the code blocks, and manually adds a button to each one. But none of these options seemed elegant.
The "Aha!" Moment: It's Just HTML!
And then it hit me. What if the component wasn't a Svelte component? What if it was a component that the browser understands all by itself, without a framework?
Event with frameworks, I figured this is the exact kind of problem Web Components were designed to solve.
A Web Component is essentially a custom, reusable HTML tag. I could create a <code-block>
tag that contained its own logic and styles. My Markdown processor doesn't need to know about Svelte, it just needs to wrap my code snippets in this special tag. The browser would then see <code-block>
and automatically bring it to life.
The Solution: A Svelte Component That's Also a Web Component
The best part? Svelte makes creating Web Components incredibly easy. I could still write my component using all the Svelte features I love—runes, reactivity, and easy event handling—and then tell Svelte to export it as a Web Component.
Here is the entire component. I named it CodeBlock.svelte
.
The magic is this one line: <svelte:options customElement={{ tag: 'code-block', shadow: 'none' }} />
. This tells Svelte: "Compile this component not for a Svelte app, but into a standard, framework-free Web Component named <code-block>
."
(Pro-tip: I used shadow: 'none'
so my global Tailwind CSS styles would work inside the component without any extra work.)
Next, I updated my marked
configuration to use this new tag. I told it to take the raw code and the highlighted HTML and pass them as attributes to my <code-block>
element.
Finally, to make it all work, I just needed to "register" my new custom element with the browser. In my SvelteKit blog post layout, all I had to do was import the component file.
And that's it. Seriously. Just by importing the file, Svelte handles the registration. Now, every time my processed Markdown includes a <code-block>
tag, the browser knows exactly how to render my interactive, Svelte-powered button.
More Resources
- MDN Web Docs: An excellent introduction to Web Components.
- Svelte Docs: Read about Svelte's
customElement
options. - My Blog's Source Code: You can see this implementation live in my blog's repository.