Skip to content
Astro as integrations 4 min read

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.

DirectiveWhen it hydratesTypical use
client:loadImmediately on page loadAbove-the-fold, critical UI
client:idleWhen the browser is idleLower-priority widgets
client:visibleWhen it enters the viewportBelow-the-fold islands
client:mediaWhen a media query matchesMobile-only or desktop-only UI
client:only="vue"Skips SSR, renders only client-sideComponents that need browser APIs

Use client:only="vue" for components that touch window, 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.

OptionTypePurpose
jsxbooleanEnable Vue JSX/TSX support
appEntrypointstringModule that extends the Vue app instance
templateobjectPass options to @vitejs/plugin-vue
devtoolsbooleanEnable Vue DevTools in development

Best Practices

  • Reach for client:visible or client:idle before client:load so 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 appEntrypoint instead of importing them in every SFC.
  • Prefer Astro components for static, content-heavy markup and reserve Vue for genuinely interactive pieces.
  • Pin @astrojs/vue and vue versions together and verify Astro compatibility before upgrading.
Last updated June 14, 2026
Was this helpful?