Use Vue in a static site with Web Component custom elements
Learn how to use Vue to create custom elements that are easy to drop into your static site's Markdown files.
Intro
Recently I rebuilt Table to Markdown, converting it from a Nuxt project to a static site built with Blurry, a pre-release static site generator ("SSG") I've been working on. I wanted the simplicity and performance of a static site, but I still needed interactivity.
"What simplicity and performance?" you ask? Well, one challenge I had with a the hybrid version is that it was hard to accurately count page views since sometimes they would be tracked in server-side code and sometimes they'd be tracked in client-side code—and I didn't want to count them twice. I had the same challenges with ad impressions, and accidentally counting those more than once could have gotten me in trouble with any ad networks I was using.
With a static site, those page views and ad impressions didn't require any thought because they would fire when the HTML was loaded, and that was that.
Using Vue for the entire site led to quite a bit of JavaScript for things that I really didn't need JavaScript for, like site navigation. Using a static site meant I could rely on regular hyperlink navigation without requiring a single line of JS, which meant faster page loads.
But, there was a catch: I still needed the interactivity I got from Vue, so I couldn't replace all of the Vue code with a static site.
Web Components with Vue
To make it easy to integrate Vue with my static site, rather than using Vue to manage the whole page, I opted to use Vue in a single custom HTML element via Web Components.
For a sneak peak of what we're building toward, here's a snippet of a Markdown file from Table to Markdown:
# Convert spreadsheet cells to Markdown
<div class="custom-element-container">
<table-converter></table-converter>
</div>
Table to Markdown makes it easy to convert cells from Microsoft Excel, [...]
Web Components allow you to create custom self-contained HTML elements that can leverage JavaScript to do whatever you want them to. A big benefit is that once you include the JS where the Web Components are defined, you can use them just like you'd use a regular HTML element, like a <p>
or <a>
.
Sounds good, eh? Almost too good? Well, there are some wrinkles with this approach, but they're nothing we can't iron out.
Wrinkles
Layout shift once web component loads
If you follow this blog, you'll have noticed that I have a thing for PageSpeed scores.
One thing to keep in mind when using Web Components is that their height isn't taken into account when the page renders.
By matching the height of your Web Components and the height of a web component wrapper, you can avoid content layout shifts, which are disruptive to your visitors:
/* Main stylesheet */
.custom-element-container {
min-height: 300px;
}
CSS scoping
To ensure that your styling is consistent in your static site and your web components, you can use the same CSS file in both places. Simply import your CSS file in your Vue component:
<style>
@import('main.css')
</style>
Child component CSS
If your single-file Vue component uses other, non-custom element Vue components with <style>
tags, you may be surprised to see those styles disappear when you use your custom element.
For some workarounds:
@import()
relevant CSS in your child components- Rather than scoping your CSS (
<style scoped>
) in your top-level component, define CSS rules that will also apply to child components- Style classes, elements, or use CSS custom properties (variables) in inline
style=""
/:style="{}"
attributes on elements in your child components
- Style classes, elements, or use CSS custom properties (variables) in inline
Implementation
Now that we've identified some wrinkles and ways to smooth them out, it's time to see how to convert a Vue file into a custom element.
Vite config
First, update the Vue plugin for Vite to generate web components, and configure Vite to place your built components where you want them:
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
// Get Vue to output custom elements
plugins: [vue({ customElement: true })],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
// Specify a directory in your SSG's build directory
outDir: 'dist/elements',
rollupOptions: {
// Improvement: loop through files in the elements directory to build this object
input: {
'your-component': resolve(__dirname, 'src/elements/your-component.ts'),
},
// Use predictable file names
output: {
entryFileNames(chunkInfo) {
return `${chunkInfo.name}.js`
},
},
}
}
})
Custom element file
In a separate file, define your Vue component as a custom element, which will make your component available to use in the HTML DOM:
// src/elements/your-component.ts
import { defineCustomElement } from 'vue'
import YourComponent from '@/components/YourComponent.vue'
customElements.define('your-component', defineCustomElement(YourComponent))
Creating a separate file for each web component can help keep your JavaScript bundles small by including only the required code for that component.
Include the JS and custom element in your SSG files
Now all that's left is do is to include your custom element .js
file in the relevant template file of your SSG:
<script src="/elements/your-component.js" type="module"></script>
<your-component></your-component>
Wrap-up
That's a wrap 🎁.
This article is wrapped up like a Vue single-file component in a Web Component custom element after following the above steps.