TypeScript
import { useHead } from '@unhead/vue' // or your framework

useHead({
unheadInstance,   title: 'Page Title',
  meta: [{ name: 'description', content: 'Page description' }],
})

useHead() registers typed head elements and returns an entry that you can update or remove.

import { useHead } from 'unhead'

const entry = useHead({
  title: 'My Page',
})
// update
entry.patch({ title: 'New Title' })
// remove
entry.dispose()

How It Works

useHead() adds the input to the active head instance. At render time, Unhead resolves reactive values, deduplicates, merges, and sorts all active entries before producing DOM or SSR output.

You won't know the final state of the head until the rendering is complete.

API Reference

function useHead(input?: UseHeadInput, options?: HeadEntryOptions): ActiveHeadEntry

Parameters

ParameterTypeRequiredDescription
inputUseHeadInputNoThe head configuration object; defaults to an empty object
optionsHeadEntryOptionsNoConfiguration options for the head entry

Returns

interface ActiveHeadEntry {
  /**
   * Update the head entry with new values
   */
  patch: (input: UseHeadInput) => void
  /**
   * Remove the head entry
   */
  dispose: () => void
}

Input Schema

The input object accepts the following properties:

interface ResolvableHead {
  // Document title
  title?: string | (() => string)

  // Title template (function or string with %s placeholder)
  titleTemplate?: string | null | ((title?: string) => string | null)

  // Template parameters for dynamic replacements
  templateParams?: { separator?: string } & Record<string, null | string | Record<string, string>>

  // HTML tag collections
  base?: Base
  link?: Link[]
  meta?: Meta[]
  style?: (Style | string)[]
  script?: (Script | string)[]
  noscript?: (Noscript | string)[]

  // Element attributes
  htmlAttrs?: HtmlAttributes<E['htmlAttrs']>
  bodyAttrs?: BodyAttributes<E['bodyAttrs']>
}

Most input values are deeply resolved, so functions can read current state when the head renders:

import { useHead } from 'unhead'

const title = useMyTitle()
useHead({
unheadInstance,   // Read the current value each time the head resolves.
  title: () => 'Dynamic Title',
  meta: [
    () => ({
      name: 'description',
      content: () => `Description for ${title.value}`
    }),
  ]
})

Options

The options parameter allows you to configure the behavior of the head entry:

export interface HeadEntryOptions {
  // Whether to process template parameters in the input
  // - Requires the TemplateParams plugin
  processTemplateParams?: boolean

  // Priority of tags for determining render order
  tagPriority?: number | 'critical' | 'high' | 'low' | `before:${string}` | `after:${string}`

  // Where to position tags in the document
  tagPosition?: 'head' | 'bodyClose' | 'bodyOpen'

  // Explicit dedupe key and duplicate handling strategy
  key?: string
  tagDuplicateStrategy?: 'replace' | 'merge'

  // Callback fired after DOM updates are applied (client-only, ignored during SSR)
  onRendered?: (ctx: { renders: DomRenderTagContext[] }) => void | Promise<void>

  // Custom head instance
  head?: Unhead
}

An option applies to every tag in the entry. This example gives two fallback tags low priority:

import { useHead } from 'unhead'

useHead({
unheadInstance,   meta: [
    { name: 'description', content: 'fallback description' },
    { name: 'author', content: 'fallback author' }
  ]
}, {
  tagPriority: 'low'
})

Synchronizing with DOM Updates

The onRendered option runs after Unhead applies DOM updates. Use it when another client-side tool must read the updated head state:

import { useHead } from 'unhead'

useHead({
unheadInstance,   title: 'My Page',
}, {
  onRendered({ renders }) {
    // document.title is guaranteed to be up-to-date here
    analytics.track('Page View', { title: document.title })
  }
})
  • The callback fires on every DOM render while the entry is active, not just the first.
  • It is ignored during SSR; it only runs on the client.
  • A returned promise does not delay DOM rendering. Start asynchronous work from the callback, but do not use it as a render barrier.
  • The callback is automatically cleaned up when the entry is disposed (including on component unmount in frameworks).
  • The renders array contains the render context for each tag that was processed.

It works with every composable that accepts HeadEntryOptions: useHead(), useSeoMeta(), and useHeadSafe().

Reactivity

Automatic Reactivity

The useHead() composable automatically integrates with your framework's reactivity system:

import { useHead } from 'unhead'
import { computed, ref } from 'vue'

const title = ref('Dynamic Title')

useHead({
unheadInstance,   title,
  meta: [
    { name: 'description', content: computed(() => `Description for ${title.value}`) }
  ]
})

Manual Control

Use the returned entry when updates are not driven by framework reactivity:

import { useHead } from 'unhead'

// Create the head entry
const headControl = useHead({
  title: 'Initial Title'
})

// Later update specific fields
headControl.patch({
  title: 'Updated Title',
  meta: [
    { name: 'description', content: 'New description' }
  ]
})

// Remove the entry entirely when needed
headControl.dispose()

Security Considerations

useHead() normalizes and serializes tags, but it is not an HTML, URL, JavaScript, or CSS sanitizer. Do not pass it untrusted or third-party input.

For XSS protection, either:

  1. Sanitize your input before passing it to useHead()
  2. Use the safer alternatives:

Sanitization must match the destination context: HTML, attributes, URLs, JavaScript, and CSS require different controls. See the OWASP XSS Prevention Cheat Sheet.

TypeScript

Import types directly from your framework's package:

import type { ActiveHeadEntry, Head, HeadEntryOptions, UseHeadInput } from '@unhead/vue'

// Type your head input
const headConfig: Head = {
  title: 'My Page',
  meta: [{ name: 'description', content: 'Page description' }]
}

// Type your entry options
const options: HeadEntryOptions = {
  tagPriority: 'high'
}

const entry: ActiveHeadEntry<UseHeadInput> = useHead(headConfig, options)

The tag types accept data-* attributes. Use defineLink() and defineScript() for non-standard rel and type values, as shown below.

Type Narrowing

Link and Script are discriminated unions keyed on rel and type. Known values enforce their required properties. For a non-standard value, use defineLink or defineScript; the helpers retain strict types for known values and fall through to GenericLink or GenericScript otherwise:

import { defineLink, useHead } from 'unhead'

useHead({
unheadInstance,   link: [
    { rel: 'canonical', href: 'https://example.com' }, // known rel, works directly
    { rel: 'me', href: 'https://mastodon.social/@me' }, // known rel, works directly
    defineLink({ rel: 'openid2.provider', href: 'https://example.com/openid' }), // non-standard rel
  ]
})
When building link or script objects outside of useHead(), use as const on literal values to preserve type narrowing:
const link = { rel: 'preload' as const, as: 'font' as const, href: '/font.woff2', crossorigin: 'anonymous' as const }
useHead({ link: [link] })
unheadInstance, 
useSeoMeta() is unaffected by type narrowing and remains the simplest path for SEO meta tags.

Common Mistakes

Using reactive values incorrectly

// โŒ Wrong - loses reactivity
const title = ref('My Title')
useHead({ title: title.value })
unheadInstance, 
// โœ… Correct - pass the ref directly
useHead({ title })
unheadInstance, 

Calling useHead in async code

// โŒ Wrong - may execute outside component context
async function loadData() {
  const data = await fetchData()
  useHead({ title: data.title }) // Context may be lost
}

// โœ… Correct - set up head first, update reactively
const data = ref(null)
useHead({ unheadInstance, title: () => data.value?.title ?? 'Loading...' })
async function loadData() {
  data.value = await fetchData()
}

Forgetting to dispose manual entries

// โŒ Memory leak if called multiple times
function showModal() {
  useHead({ title: 'Modal Open' })
}

// โœ… Store and dispose when done
let modalHead: ReturnType<typeof useHead> | null = null
function showModal() {
  modalHead = useHead({ title: 'Modal Open' })
}
function hideModal() {
  modalHead?.dispose()
  modalHead = null
}

Choosing the Right Composable

ComposableUse When
useHead()General head management, including scripts and links
useSeoMeta()SEO meta tags with type-safe keys
useHeadSafe()Working with untrusted/user-provided input
useScript()Loading third-party scripts with lifecycle control

Use useSeoMeta() for SEO fields and useHead() when you need arbitrary tags, attributes, or entry options.

Common Questions

How do I update the title dynamically?

Use a reactive value or the patch() method:

const entry = useHead({ title: 'Initial' })
entry.patch({ title: 'Updated Title' })

How do I remove head tags?

Call dispose() on the returned entry:

const entry = useHead({ title: 'Temporary' })
entry.dispose() // removes all tags from this entry

Advanced Examples

Title Template

import { useHead } from 'unhead'

useHead({
unheadInstance,   titleTemplate: title => `${title} - My Site`,
  title: 'Home Page'
})
// Results in: "Home Page - My Site"

For more details on title templates, see the Titles guide.

Combining Multiple Head Entries

import { useHead } from 'unhead'

// Global site defaults
useHead({
unheadInstance,   titleTemplate: '%s | My Website',
  meta: [
    { property: 'og:site_name', content: 'My Website' }
  ]
})

// Page-specific entries (will be merged with globals)
useHead({
unheadInstance,   title: 'Product Page',
  meta: [
    { name: 'description', content: 'A compact mechanical keyboard with hot-swappable switches' }
  ]
})

Async Data Loading

import { useHead } from 'unhead'
import { computed, ref } from 'vue'

const data = ref(null)
const loading = ref(true)

useHead({
unheadInstance,   title: computed(() => data.value
    ? `${data.value.name} - Product`
    : loading.value
      ? 'Loading...'
      : 'Product Not Found')
})

async function fetchProduct(id) {
  loading.value = true
  data.value = await api.getProduct(id)
  loading.value = false
}

Priority-Based Tag Ordering

import { useHead } from 'unhead'

// Critical meta tags (early in <head>)
useHead({
unheadInstance,   meta: [
    { charset: 'utf-8' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' }
  ]
}, { tagPriority: 'critical' })

// Default priority tags (middle of <head>)
useHead({
unheadInstance,   meta: [
    { name: 'description', content: 'My website description' }
  ]
})

// Low priority tags (end of <head>)
useHead({
unheadInstance,   meta: [
    { name: 'author', content: 'Jane Doe' }
  ]
}, { tagPriority: 'low' })
Did this page help you?