Vue Vapor Mode
Use the Vue composables inside Vapor component setup, with the same head instance installed on your app. Check the lifecycle limits below before converting components that own page metadata.
This guide describes behavior checked with Vue 3.6.0-rc.11.
The cleanup behavior requires the changes in head cleanup #1020 and script cleanup #1021.
Check those changes against your installed Unhead version. A passing fixture does not establish support in an earlier release.
Setup and reactive metadata
Follow the Vue installation guide to install @unhead/vue and register createHead() through app.use(head).
Use a Vue compiler that supports the vapor attribute.
If your app uses createApp(), install Vue's vaporInteropPlugin to render Vapor components inside it.
See Vue's Vapor setup and compatibility reference for app configuration.
Create head entries synchronously during setup:
<script setup lang="ts" vapor>
import { useHead } from '@unhead/vue'
const props = defineProps<{ title: string }>()
useHead({
title: () => props.title,
meta: [{ name: 'robots', content: 'noindex' }],
})
</script>
<template>
<p>{{ title }}</p>
</template>
When the parent changes title, Unhead updates the document title.
With the head cleanup change, unmounting this component removes its head entry, including the robots tag.
Any remaining parent head entry can then supply the title.
useSeoMeta() and useHeadSafe() use the same head lifecycle path.
The dedicated Vapor fixture directly exercises useHead().
Lifecycle limits
Vue's public getCurrentInstance() returns null inside Vapor components.
Unhead uses the active effect scope for unmount cleanup when no VDOM component instance exists.
See Vue's feature compatibility list for instance-dependent APIs.
| Situation | Behavior and action |
|---|---|
| Reactive props, refs, or getters | Updates patch the existing head entry. |
Unmount through v-if or app teardown | With head cleanup #1020, stopping the owner scope disposes the entry. |
Vapor child cached by <KeepAlive> | Automatic head deactivation is unsupported. Its title and tags can remain while inactive. |
| Calls after an asynchronous callback loses setup context | Create the entry before awaiting. Update reactive values or patch that entry afterward. |
| Calls without an active effect scope | Provide the head explicitly and dispose entries yourself. Injection alone does not establish cleanup ownership. |
For cached Vapor pages, keep page metadata in a VDOM component that owns the activation lifecycle.
Alternatively, use v-if without <KeepAlive> when unmounting is acceptable.
Changing a component to v-show preserves its scope and head entries.
For asynchronous work, follow Create the Entry Before Awaiting.
Passing an explicit head after an await restores access to Unhead, but does not restore the original effect scope.
Script consumers
With script cleanup #1021, useScript() captures the effect scope that owns the consumer.
On scope disposal, it releases that consumer's load and error callbacks.
Callbacks registered later through a retained handle still belong to the captured scope.
Registrations through a stopped owner's handle are inert.
With { scope: true }, disposal also aborts the consumer's signal and cancels its pending triggers.
Other consumers can continue using the shared script.
Unmount cleanup keeps the shared script tag available for reuse; it does not undo JavaScript that already executed.
The default loading trigger differs between rendering modes.
The Vue adapter defers the default trigger to onMounted when it has a VDOM instance.
Vapor falls through to the core client trigger.
If you require loading after mount, pass an explicit callback trigger:
<script setup lang="ts" vapor>
import { useScript } from '@unhead/vue'
import { onMounted } from 'vue'
useScript('https://example.com/analytics.js', {
trigger: load => onMounted(load),
scope: true,
})
</script>
<template>
<span>Analytics example</span>
</template>
Replace the example URL with a trusted script URL.
See the useScript() reference for manual loading and shared script ownership.
Coverage and boundaries
The Vapor fixture compiles a real Vapor SFC and bundles Vue's Vapor browser runtime.
It runs in JSDOM with a VDOM parent and vaporInteropPlugin.
It checks reactive titles, repeated unmount and remount, script callbacks, and shared consumers.
This fixture does not verify pure createVaporApp() apps, browser execution, SSR, or hydration.
KeepAlive deactivation is a known separate limitation.
Use composables for this integration. The fixture does not establish Vapor compatibility for Unhead's render-function components or Options API integration.