--- title: useHead() description: Manage document head tags with useHead(). Set titles, meta tags, scripts, and styles with typed input and reactive updates. --- ```ts import { useHead } from '@unhead/vue' // or your framework useHead({ title: 'Page Title', meta: [{ name: 'description', content: 'Page description' }], }) ``` `useHead()`{lang="ts"} 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. :: ## API Reference ```ts function useHead(input?: UseHeadInput, options?: HeadEntryOptions): ActiveHeadEntry ``` ### Parameters | Parameter | Type | Required | Description | | --------- | ------------------ | -------- | ------------------------------------------------------------- | | `input` | `UseHeadInput` | No | The head configuration object; defaults to an empty object | | `options` | `HeadEntryOptions` | No | Configuration options for the head entry | ### 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> // HTML tag collections base?: Base link?: Link[] meta?: Meta[] style?: (Style | string)[] script?: (Script | string)[] noscript?: (Noscript | string)[] // Element attributes htmlAttrs?: HtmlAttributes bodyAttrs?: BodyAttributes } ``` 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 // 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()`{lang="ts"} composable automatically integrates with your framework's reactivity system: ::FrameworkCode #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}`) } ] }) ``` #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
My Page
} ``` #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
My Page
} ``` :: ::VueOnly 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 :: ### 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()`{lang="ts"} 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()`{lang="ts"} 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, HeadEntryOptions, UseHeadInput } from '@unhead/vue' // Type your head input const headConfig: UseHeadInput = { title: 'My Page', meta: [{ name: 'description', content: 'Page description' }] } // Type your entry options const options: HeadEntryOptions = { tagPriority: 'high' } const entry: ActiveHeadEntry = useHead(headConfig, options) ``` The `Head` type is deprecated. Use `UseHeadInput` for anything you pass to `useHead()`{lang="ts"}. Use `SerializableHead` when the object must stay plain, for example JSON you send over the wire. The tag types accept `data-*` attributes. Use `defineLink()` and `defineScript()` for non-standard `rel` and `type` values, as shown below. ### Typing Tag Arrays To build a tag array before you call `useHead()`{lang="ts"}, use the per-tag types. Do not write your own `{ rel: string, href: string }` shape; `rel` must stay a literal so the union can narrow. ```ts import type { Link } from '@unhead/vue' import { useHead } from '@unhead/vue' const link: Link[] = [] link.push({ rel: 'canonical', href: 'https://example.com' }) for (const locale of availableLocales) { link.push({ rel: 'alternate', href: `https://example.com/${locale.code}`, hreflang: locale.code, }) } useHead({ link }) ``` These types also carry the Unhead options for that tag, such as `key`, `tagPriority` and `tagPosition`. Alternate links deduplicate on `hreflang` already, so they need no `key`. For a non-standard `rel`, wrap that entry in `defineLink()`{lang="ts"}, see [Type Narrowing](#type-narrowing). To build the head object itself, type it as `ReactiveHead` in Vue, or `ResolvableHead` elsewhere. Keep the array in its own variable. Each tag array can also be a function, so you cannot push onto `head.link` directly. ```ts import type { ReactiveHead, ResolvableLink } from '@unhead/vue' import { useHead } from '@unhead/vue' const head: ReactiveHead = {} const link: ResolvableLink[] = [] link.push({ rel: 'canonical', href: 'https://example.com' }) head.link = link useHead(head) ``` `ResolvableLink` allows a ref, a computed, or a getter for each property. If every value is static, use `Link` instead. ### 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 ] }) ``` :: ::tip When building link or script objects outside of `useHead()`{lang="ts"}, 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 `useSeoMeta()`{lang="ts"} is unaffected by type narrowing and remains the simplest path for SEO meta tags. :: ## 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 | null = null function showModal() { modalHead = useHead({ title: 'Modal Open' }) } function hideModal() { modalHead?.dispose() modalHead = null } ``` ## Choosing the Right Composable | Composable | Use 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: ```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 ::FrameworkCode #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 } ``` #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
Product Page
} ``` #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
Product Page
} ``` :: ### Priority-Based Tag Ordering ```ts import { useHead } from '@unhead/dynamic-import' // Critical meta tags (early in ) useHead({ meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' } ] }, { tagPriority: 'critical' }) // Default priority tags (middle of ) useHead({ meta: [ { name: 'description', content: 'My website description' } ] }) // Low priority tags (end of ) useHead({ meta: [ { name: 'author', content: 'Jane Doe' } ] }, { tagPriority: 'low' }) ```