---
title: "useHead()"
description: "Manage document head tags with useHead(). Set titles, meta tags, scripts, and styles with typed input and reactive updates."
canonical_url: "https://unhead.unjs.io/docs/head/api/composables/use-head"
last_updated: "2026-08-08T19:06:50.069Z"
---

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

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

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

```ts
import { useHead } from '@unhead/dynamic-import'

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](/docs/head/guides/core-concepts/handling-duplicates),
merges, and [sorts](/docs/head/guides/core-concepts/positions) all active entries before producing DOM or SSR output.

<note>

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

</note>

## API Reference

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

### Parameters

<table>
<thead>
  <tr>
    <th>
      Parameter
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Required
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        input
      </code>
    </td>
    
    <td>
      <code>
        UseHeadInput
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      The head configuration object; defaults to an empty object
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options
      </code>
    </td>
    
    <td>
      <code>
        HeadEntryOptions
      </code>
    </td>
    
    <td>
      No
    </td>
    
    <td>
      Configuration options for the head entry
    </td>
  </tr>
</tbody>
</table>

### Returns

```ts
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:

```ts
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:

```ts
import { useHead } from '@unhead/dynamic-import'

const title = useMyTitle()
useHead({
  // 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:

```ts
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:

```ts
import { useHead } from '@unhead/dynamic-import'

useHead({
  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:

```ts
import { useHead } from '@unhead/dynamic-import'

useHead({
  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:

<framework-code>
<template v-slot:vue="">

```ts
import { useHead } from '@unhead/dynamic-import'
import { computed, ref } from 'vue'

const title = ref('Dynamic Title')

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

</template>

<template v-slot:react="">

```tsx
import { useHead } from '@unhead/dynamic-import'
import { useState } from 'react'

function MyPage() {
  const [title, setTitle] = useState('Dynamic Title')

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

  return <div>My Page</div>
}
```

</template>

<template v-slot:solid="">

```tsx
import { useHead } from '@unhead/dynamic-import'
import { createSignal } from 'solid-js'

function MyPage() {
  const [title, setTitle] = createSignal('Dynamic Title')

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

  return <div>My Page</div>
}
```

</template>
</framework-code>

<vue-only>

Vue automatically:

- Tracks reactive data changes with `watchEffect`
- Resolves refs, computed props, and reactive objects
- Cleans up head entries on component unmount
- Handles special cases like keep-alive components

</vue-only>

### Manual Control

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

```ts
import { useHead } from '@unhead/dynamic-import'

// 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

<warning>

`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.

</warning>

For XSS protection, either:

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

  - [useSeoMeta()](/docs/head/api/composables/use-seo-meta) for SEO metadata
  - [useHeadSafe()](/docs/head/api/composables/use-head-safe) for general head management

Sanitization must match the destination context: HTML, attributes, URLs, JavaScript, and CSS require different controls. See the [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).

## TypeScript

Import types directly from your framework's package:

```ts
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:

<code-group>

```ts [Non-standard Link rel]
import { defineLink, useHead } from '@unhead/dynamic-import'

useHead({
  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
  ]
})
```

```ts [Custom Script type]
import { defineScript, useHead } from '@unhead/dynamic-import'

useHead({
  script: [
    { src: 'https://example.com/app.js' }, // external script, works directly
    defineScript({ type: 'text/plain', textContent: '...' }), // custom type
  ]
})
```

</code-group>

<tip>

When building link or script objects outside of `useHead()`, use `as const` on literal values to preserve type narrowing:

```ts
const link = { rel: 'preload' as const, as: 'font' as const, href: '/font.woff2', crossorigin: 'anonymous' as const }
useHead({ link: [link] })
```

</tip>

<tip>

`useSeoMeta()` is unaffected by type narrowing and remains the simplest path for SEO meta tags.

</tip>

## Common Mistakes

### Using reactive values incorrectly

```ts
// ❌ Wrong - loses reactivity
const title = ref('My Title')
useHead({ title: title.value })

// ✅ Correct - pass the ref directly
useHead({ title })
```

### Calling useHead in async code

```ts
// ❌ 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({ title: () => data.value?.title ?? 'Loading...' })
async function loadData() {
  data.value = await fetchData()
}
```

### Forgetting to dispose manual entries

```ts
// ❌ 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

<table>
<thead>
  <tr>
    <th>
      Composable
    </th>
    
    <th>
      Use When
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        useHead()
      </code>
    </td>
    
    <td>
      General head management, including scripts and links
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useSeoMeta()
      </code>
    </td>
    
    <td>
      SEO meta tags with type-safe keys
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useHeadSafe()
      </code>
    </td>
    
    <td>
      Working with untrusted/user-provided input
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        useScript()
      </code>
    </td>
    
    <td>
      Loading third-party scripts with lifecycle control
    </td>
  </tr>
</tbody>
</table>

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:

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

### How do I remove head tags?

Call `dispose()` on the returned entry:

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

## Advanced Examples

### Title Template

```ts
import { useHead } from '@unhead/dynamic-import'

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

For more details on title templates, see the [Titles guide](/docs/head/guides/core-concepts/titles).

### Combining Multiple Head Entries

```ts
import { useHead } from '@unhead/dynamic-import'

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

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

### Async Data Loading

<framework-code>
<template v-slot:vue="">

```ts
import { useHead } from '@unhead/dynamic-import'
import { computed, ref } from 'vue'

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

useHead({
  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
}
```

</template>

<template v-slot:react="">

```tsx
import { useHead } from '@unhead/dynamic-import'
import { useState } from 'react'

function ProductPage({ id }) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)

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

  return <div>Product Page</div>
}
```

</template>

<template v-slot:solid="">

```tsx
import { useHead } from '@unhead/dynamic-import'
import { createSignal } from 'solid-js'

function ProductPage(props) {
  const [data, setData] = createSignal(null)
  const [loading, setLoading] = createSignal(true)

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

  return <div>Product Page</div>
}
```

</template>
</framework-code>

### Priority-Based Tag Ordering

```ts
import { useHead } from '@unhead/dynamic-import'

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

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

// Low priority tags (end of <head>)
useHead({
  meta: [
    { name: 'author', content: 'Jane Doe' }
  ]
}, { tagPriority: 'low' })
```
