---
title: "How to Set Page Titles in Vue 3"
description: "Set dynamic page titles in Vue 3 with useHead. Learn title templates, reactive titles, and SSR patterns that include titles in the initial HTML response."
canonical_url: "https://nuxtseo.com/learn-seo/vue/mastering-meta/titles"
last_updated: "2026-10-08"
---

> Index of every page on this site: [llms.txt](https://nuxtseo.com/llms.txt)

::key-takeaways
- Use `useHead()`{lang="ts"} instead of `document.title` for SSR compatibility
- Add a title template to append your site name consistently
- Write concise, descriptive titles. Google truncates title links to fit the device width
- Set `og:title` separately for social sharing
::

Page titles appear in browser tabs. Google uses your title and other page signals to choose a search title link. Write a clear title that matches the page. [Google explains the sources it uses](https://developers.google.com/search/docs/appearance/title-link).

```html
<head>
  <title>Mastering Titles in Vue · Vue SEO</title>
</head>
```

Setting titles in Vue 3 requires a head manager like [Unhead](https://unhead.unjs.io/) since `document.title`{lang="ts"} won't work during server-side rendering.

## Quick Reference

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'

// Basic title
useHead({ title: 'Home' })

// With template (adds site name)
useHead({
  title: 'Home',
  titleTemplate: '%s | MySite'
})
</script>
```

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

// Reactive title from data
const post = ref({ title: 'Loading...' })
useHead({
  title: () => post.value.title
})
</script>
```

```vue
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

// SEO-focused (includes og:title)
useSeoMeta({
  title: 'Home',
  ogTitle: 'Home | MySite'
})
</script>
```

## Why Not `document.title`?

You might try setting titles directly:

```ts
// ❌ Breaks SSR, may not be indexed
document.title = 'Home'
```

This fails during server-side rendering: the title won't exist in the initial HTML response. Google can [render JavaScript](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics). Rendering can fail when scripts or resources are unavailable.

::warning
Using `document.title` in a Vue SPA means search engines may index your pages with missing or incorrect titles. Always use a head manager like Unhead for SEO-critical metadata.
::

Use [Unhead](https://unhead.unjs.io/) instead. It handles both SSR and client-side updates. For Google's official guidance, see [Influencing your title links](https://developers.google.com/search/docs/appearance/title-link).

## Setting Titles with `useHead()`{lang="ts"}

The [`useHead()`{lang="ts"}](https://unhead.unjs.io/docs/head/api/composables/use-head) composable sets titles that work in SSR and client-side navigation:

```vue twoslash [input.vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home'
})
</script>
```

```html [output.html]
<head>
  <title>Home</title>
</head>
```

You can set other head tags in the same call from any component:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home',
  meta: [
    { name: 'description', content: 'Welcome to MyApp' }
  ]
})
</script>
```

### Reactive Titles

::tip
Unhead accepts refs, reactive objects, and computed values. Pass the reactive reference directly; don't destructure it with `.value`.
::

::code-group
```vue [Bad]
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

const myTitle = ref('Home')

// Loses reactivity
useHead({
  title: myTitle.value
})
</script>
```

```vue [Good]
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

const myTitle = ref('Home')

// Stays reactive
useHead({
  title: myTitle
})
</script>
```
::

Computed getter syntax works for derived titles:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

const post = ref({ title: 'Loading...' })

useHead({
  title: () => post.value.title // Updates when post changes
})
</script>
```

### SSR and SEO

Fetch data before render, not in `onMounted()`{lang="ts"}. Client-only fetches leave the loading state in the initial HTML response:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { onMounted, ref } from 'vue'

const postTitle = ref('Loading...')
useHead({ title: postTitle })

// onMounted runs after SSR. Initial HTML contains "Loading..."
onMounted(() => {
  (async () => {
    postTitle.value = (await fetchPostData()).title
  })()
})
</script>
```

Use SSR-compatible data fetching (Vue's `onServerPrefetch()`{lang="ts"} or framework solutions like Nuxt's `useFetch()`{lang="ts"}).

## Title Templates

Most sites append a site name to titles for brand recognition. [Google recommends](https://developers.google.com/search/docs/appearance/title-link#page-titles) adding your site name with a delimiter:

```html
<head>
  <title>Home | MySite</title>
</head>
```

Use `titleTemplate` with a [title template](https://unhead.unjs.io/docs/head/guides/core-concepts/titles):

```vue twoslash [input.vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home',
  titleTemplate: '%s | MySite'
})
</script>
```

```html [output.html]
<head>
  <title>Home | MySite</title>
</head>
```

The `%s` token gets replaced with your page title (or empty string if none set).

Override the template for specific pages by passing `null`:

```vue [input.vue]
<script lang="ts" setup>
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home',
  titleTemplate: null
})
</script>
```

```html [output.html]
<head>
  <title>Home</title>
</head>
```

## Template Params

For dynamic site names or separators, register the [Template Params plugin](https://unhead.unjs.io/docs/head/guides/plugins/template-params) when creating your head instance:

```ts
import { createHead } from '@unhead/vue/client'
import { TemplateParamsPlugin } from '@unhead/vue/plugins'

const head = createHead({
  plugins: [TemplateParamsPlugin()]
})
```

Then use custom params:

```vue twoslash [input.vue]
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home',
  titleTemplate: '%s %separator %siteName',
  templateParams: {
    separator: '·',
    siteName: 'MySite'
  }
})
</script>
```

```html [output.html]
<head>
  <title>Home · MySite</title>
</head>
```

Common separators: `|` `-` `.` `•` `·`

Template params work in meta tags too:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'

useHead({
  templateParams: { siteName: 'MyApp' },
  title: 'Home',
  meta: [
    { name: 'description', content: 'Welcome to %siteName' },
    { property: 'og:title', content: 'Home | %siteName' }
  ]
})
</script>
```

## Social Share Titles

Social platforms use `og:title` and `twitter:title` meta tags. Use [`useSeoMeta()`{lang="ts"}](https://unhead.unjs.io/docs/head/api/composables/use-seo-meta) to set these:

::figure-image{src="/nuxt-x-share.png" alt="Nuxt X Share" lazy="true"}
::

```vue [input.vue]
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'Why you should eat more broccoli',
  titleTemplate: '%s | Health Tips',
  // og:title ignores titleTemplate, set it explicitly
  ogTitle: 'Health Tips: 10 reasons to eat more broccoli',
  // Set X title explicitly when you need a specific preview
  twitterTitle: 'Hey X! 10 reasons to eat more broccoli',
})
</script>
```

```html [output.html]
<head>
  <title>Why you should eat more broccoli | Health Tips</title>
  <meta property="og:title" content="Health Tips: 10 reasons to eat more broccoli" />
  <meta name="twitter:title" content="Hey X! 10 reasons to eat more broccoli" />
</head>
```

Set `twitter:title` explicitly when the X preview needs a specific title. Test the current preview on X.

## Titles for AI Search

Use a title that describes the page. "How to Set Meta Tags in Vue 3" identifies a tutorial. "Vue 3 Meta Tag API Reference" identifies documentation. These titles help readers choose the right page. They do not guarantee an AI citation.

## Title Length

Google sets no fixed title character limit. It truncates title links to fit the device width. Two titles of equal length can display differently. Treat 50-60 characters as an editing guide.

A short title can still be rewritten. Google chooses the title link based on the page and its other signals.

A longer title can still help describe the page. Put the main topic first, since Google truncates the displayed title to fit the device width.

## Vue Router Integration

Set titles from route meta for centralized title management. These excerpts assume your Vue app has installed both Vue Router and an Unhead instance. Create a fresh app, router, and head for each SSR request:

```ts
// router.ts
const routes = [
  { path: '/', component: Home, meta: { title: 'Home' } },
  { path: '/about', component: About, meta: { title: 'About Us' } },
  { path: '/blog/:slug', component: BlogPost, meta: { title: 'Blog' } }
]
```

Use a navigation guard to apply titles on route change:

```ts
import { useHead } from '@unhead/vue'

router.afterEach((to) => {
  const title = to.meta.title as string
  if (title) {
    useHead({ title })
  }
})
```

For dynamic routes, override in the component:

```vue
<script setup lang="ts">
import { useHead } from '@unhead/vue'

// Fetched post data overrides route meta
// Excerpt: route and fetchPost come from your application.
const post = await fetchPost(route.params.slug)

useHead({
  title: post.title // "How to Build a Blog" instead of generic "Blog"
})
</script>
```

Set a global title template in your guard:

```ts
import { useHead } from '@unhead/vue'

router.afterEach((to) => {
  useHead({
    title: to.meta.title as string || 'MySite',
    titleTemplate: '%s | MySite'
  })
})
```

Handle nested routes by walking matched routes:

```ts
import { useHead } from '@unhead/vue'

router.afterEach((to) => {
  // Find deepest route with a title
  const title = to.matched.toReversed()
    .find(r => r.meta.title)
    ?.meta
    .title as string

  if (title)
    useHead({ title })
})
```

Using Nuxt? Check out [Nuxt SEO](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction) which handles much of this automatically. [Learn more about Page Titles in Nuxt →](https://nuxtseo.com/learn-seo/nuxt/mastering-meta/titles)

After deploying a title edit, check the live HTML and Google's last crawl separately. [Reported title updates](https://www.seroundtable.com/google-crawling-indexing-serving-data-42225.html) typically took 1 to 2 days at the serving stage, with slow cases lasting weeks or months.

Recrawling and reprocessing add separate waits. Google can choose another title. See [Google Search timelines](https://nuxtseo.com/learn-seo/google-search-timelines).

## Checklist

::checklist{#vue-titles}
- Use `useHead()`{lang="ts"} or `useSeoMeta()`{lang="ts"} instead of `document.title`
- Set up a title template with your site name
- Keep titles concise and check their display width
- Set `og:title` for social sharing
- Fetch data server-side for dynamic titles
- Test titles render correctly in SSR
::

## Sitemap

See the full [sitemap](https://nuxtseo.com/sitemap.md) for all pages.
