Vue Integration
The @astrojs/vue integration lets you author Vue 3 single-file components (.vue) and render them inside Astro pages — either as static HTML at build time or as hydrated interactive islands. You get Vue’s Composition API, reactivity, and SFC ergonomics while Astro keeps shipping zero JavaScript by default, hydrating only the components you explicitly mark. This is the recommended way to bring an existing Vue codebase or favorite Vue libraries into an Astro site.
Installation
The fastest path is the astro add CLI, which installs the package, registers the renderer in your config, and adds the matching vue peer dependency.
npx astro add vue
This produces a config that looks like the following. The integration is a call to the factory — note the parentheses.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [vue()],
});
If you prefer to install by hand, add both packages and register the renderer yourself:
npm install @astrojs/vue vue
Authoring a single-file component
Once the renderer is registered, place standard .vue SFCs anywhere in src/. They support <script setup>, the Composition API, scoped styles, and <template> exactly as in a plain Vue app.
<!-- src/components/Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const { start = 0 } = defineProps<{ start?: number }>();
const count = ref(start);
</script>
<template>
<button @click="count++">Count is {{ count }}</button>
</template>
<style scoped>
button { font-weight: 600; }
</style>
Rendering and hydrating in .astro files
Import the component into a .astro file’s component-script fence and use it like any other element. Without a client:* directive it renders to static HTML with no JavaScript. Add a directive to ship the runtime and hydrate it into an interactive island.
---
// src/pages/index.astro
import Counter from '../components/Counter.vue';
---
<h1>Static heading — zero JS</h1>
<!-- Renders as HTML only, never hydrates -->
<Counter start={0} />
<!-- Hydrates when it scrolls into view -->
<Counter start={10} client:visible />
The heading and the first counter ship no client JavaScript. Only the second counter loads Vue’s runtime, and only once it enters the viewport. Props pass as normal Astro attributes and are serialized to the client.
| Directive | When it hydrates | Typical use |
|---|---|---|
client:load | Immediately on page load | Above-the-fold, critical UI |
client:idle | When the browser is idle | Lower-priority widgets |
client:visible | When it enters the viewport | Below-the-fold islands |
client:media | When a media query matches | Mobile-only or desktop-only UI |
client:only="vue" | Skips SSR, renders only client-side | Components that need browser APIs |
Use
client:only="vue"for components that touchwindow,localStorage, or other browser-only APIs at setup time. It tells Astro to skip server rendering and use the Vue renderer on the client.
Passing slots and children
Astro maps its slot syntax onto Vue slots. Default children become Vue’s default slot, and named Astro slots become named Vue slots.
---
import Card from '../components/Card.vue';
---
<Card>
<h2 slot="title">Pricing</h2>
<p>Everything you need to ship.</p>
</Card>
<!-- src/components/Card.vue -->
<template>
<article>
<header><slot name="title" /></header>
<div><slot /></div>
</article>
</template>
Configuring the renderer
The vue() factory accepts options to enable JSX, register Vue plugins app-wide, or tweak @vitejs/plugin-vue behavior. A common need is an appEntrypoint to install plugins (router, i18n, Pinia) on every Vue island.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
export default defineConfig({
integrations: [
vue({
jsx: true,
appEntrypoint: '/src/pages/_app',
}),
],
});
// src/pages/_app.ts
import type { App } from 'vue';
import { createPinia } from 'pinia';
export default (app: App) => {
app.use(createPinia());
};
The exported function receives the Vue App instance before each island mounts, so you can register global plugins, directives, or components once.
| Option | Type | Purpose |
|---|---|---|
jsx | boolean | Enable Vue JSX/TSX support |
appEntrypoint | string | Module that extends the Vue app instance |
template | object | Pass options to @vitejs/plugin-vue |
devtools | boolean | Enable Vue DevTools in development |
Best Practices
- Reach for
client:visibleorclient:idlebeforeclient:loadso islands hydrate only when needed. - Keep islands small and self-contained; pass server-fetched data down as props rather than fetching inside hydrated components.
- Use
client:only="vue"for components that depend on browser-only APIs to avoid SSR hydration mismatches. - Centralize shared plugins (Pinia, i18n) in a single
appEntrypointinstead of importing them in every SFC. - Prefer Astro components for static, content-heavy markup and reserve Vue for genuinely interactive pieces.
- Pin
@astrojs/vueandvueversions together and verify Astro compatibility before upgrading.