Skip to content
Wei Li
Go back

Using Vue Components in Posts

Updated:
Edit page

This site has the @astrojs/vue integration installed, so any .vue single-file component can be used in an MDX post or an .astro page. This post covers why that’s worth having on a static blog, what actually happens to a Vue component, and the rules for using one.

An interactive counter

0
A Vue component, hydrated when it scrolls into view.

The component lives in src/components/Counter.vue. This post imports it and renders it like any other component:

import Counter from "@/components/Counter.vue";

<Counter client:visible>A Vue component, hydrated when it scrolls into view.</Counter>

Why add Vue to an Astro site?

Astro on its own is very good at one thing: turning components into static HTML at build time. An .astro component runs once, during the build, and never again. That’s why this site is fast and costs nothing to host.

For interactivity, Astro gives you a plain <script> tag. That’s enough for small things, like the theme toggle or the copy buttons on code blocks. It gets painful once a widget has real state. With plain DOM code you find elements, set their text, add and remove classes, and keep every part of the page consistent with every change by hand.

Vue takes that job over. You declare the state, describe how the page depends on it, and Vue keeps the two in sync. The Stream pipeline playground is a good example:

Vue also brings the building blocks such widgets need:

On top of that comes the Vue ecosystem: chart libraries, form validation, Pinia for shared state, and Vue’s devtools.

What runs at build time, and what runs in the browser

A Vue component always runs at build time. Whether it also runs in the browser depends on a client:* directive.

Without a directive, Astro runs the component once in Node during the build and keeps only the HTML it produces. No Vue code reaches the browser. The counter below starts at 5, but its buttons do nothing, because nothing is listening for clicks:

5

With a directive, the browser gets two things:

  1. The same HTML, generated at build time, so the component appears immediately, even before any JavaScript runs.
  2. The component’s JavaScript plus the Vue runtime. When the directive’s condition is met, Vue starts up in the browser and attaches itself to the existing HTML. This step is called hydration. From then on, clicks and typing update the component like in any Vue app.

The directive decides when hydration happens:

DirectiveHydrates
client:loadImmediately on page load
client:idleOnce the browser is idle
client:visibleWhen the component scrolls into view
client:mediaWhen a CSS media query matches
client:only="vue"In the browser only, skipping server rendering

This is Astro’s islands model: the page is static HTML, with a few interactive islands where you asked for them. Everything outside the islands stays plain HTML.

Why this works on GitHub Pages

GitHub Pages can only serve files; it can’t run code for each request. That’s fine, because nothing here needs a server:

What wouldn’t work here is anything that needs a server at request time: rendering pages on demand, API routes, or keeping secrets. A Vue component can still call a public API from the browser.

What it costs

Measured from this site’s build, gzipped:

DownloadSize
Vue runtime, shared by all islandsabout 33 KB
Counterunder 1 KB
Stream playground / collections explorer3–4 KB each
Regex testerabout 6 KB

The runtime is downloaded once per page, and only by pages with at least one hydrated component. Every other page on this site loads no Vue code at all.

When not to use Vue

Why the post has to be MDX

Vue components only work in .mdx posts, not .md or .adoc ones.

A .md file is text that becomes HTML. There’s no step that runs code, and no way to import anything. <Counter /> in a Markdown file would just be an unknown HTML tag that renders nothing. AsciiDoc works the same way here: Asciidoctor turns the file into an HTML string, which goes into the page as-is.

MDX is Markdown plus JSX. An .mdx file is compiled into a JavaScript module, the same kind as an .astro component, which gives it the two things a component needs:

So only posts that embed a component need to be .mdx; everything else can stay Markdown or AsciiDoc.

Styling

Components use the site’s Tailwind classes and theme colors (bg-accent, border-border, text-muted-foreground), so they follow the light/dark toggle automatically.

Inside a post, give the component’s outer element not-prose text-foreground:

More examples


Edit page
Share this post:

Previous Post
Interactive Java Examples with Vue
Next Post
Using Alpine.js in Astro: Why and How