---
title: "Install Unhead in TypeScript Projects"
description: "Set up Unhead with pure TypeScript. Framework-agnostic head management with createHead() and useHead() for SSR and client-side apps."
canonical_url: "https://unhead.unjs.io/docs/typescript/head/guides/get-started/installation"
last_updated: "2026-07-21T05:15:29.068Z"
---

Install `unhead`, create a head with `createHead()`, and call `useHead()` directly. SSR uses a separate server instance and `renderSSRHead()`.

This setup is for TypeScript applications without a framework adapter. If you use a framework, select its adapter below.

<framework-selector-minimal className="mb-7">



</framework-selector-minimal>

## Demos

- [StackBlitz - Unhead - Vite + TS SSR](https://stackblitz.com/edit/github-hhxywsb5)

## Install Unhead

Install the `unhead` dependency:

<module-install name="unhead@next">



</module-install>

## Client-Side Rendering

Create the browser instance from `unhead/client`. In a Vite SSR app this usually belongs in `entry-client.ts`; in an SPA, put it in the main entry.

```ts [entry-client.ts]
import { createHead } from 'unhead/client'
import { setupCounter } from './counter'
import './style.css'
import './typescript.svg'

declare global {
  interface Window {
    __UNHEAD__: ReturnType<typeof createHead>
  }
}

window.__UNHEAD__ = createHead()

setupCounter(document.querySelector('#counter') as HTMLButtonElement)
```

This example uses `window.__UNHEAD__` to keep the first setup short. For application code, wrap the composables in your own context instead of relying on a global.

<note>

Follow the [Wrapping Composables](/docs/typescript/head/guides/core-concepts/wrapping-composables) guide for the recommended way to handle context.

</note>

## Server-Side Rendering

<note>

Serving your app as an SPA? You can [skip](#add-head-tags) this step.

</note>

Somewhere in your server entry, create a server head instance.

```ts [main.ts]
import { createHead } from 'unhead/server'
export function render(_url: string) {
  const head = createHead()
  const html = `<!-- your html -->`
  return { html, head }
}
```

After rendering the application, pass the head instance and complete template to `transformHtmlTemplate()`:

```ts [server.ts]
import { transformHtmlTemplate } from 'unhead/server'
// ...

// Serve HTML
app.use('*all', async (req, res) => {
  try {
    // ...

    const rendered = await render(url)
    const html = transformHtmlTemplate(
      rendered.head,
      template.replace(`<!--app-html-->`, rendered.html ?? '')
    )

    res.status(200).set({ 'Content-Type': 'text/html' }).send(html)
  }
  catch (e) {
    // ...
  }
})
// ..
```

## Add Head Tags

The server head inserts three defaults:

- `<meta charset="utf-8">`
- `<meta name="viewport" content="width=device-width, initial-scale=1">`
- `<html lang="en">`

Set server-only defaults through `init`. Keeping them in the server entry also keeps them out of the client bundle.

```ts [main.ts]
import { createHead } from 'unhead/server'
export function render(_url: string) {
  const head = createHead({
    // change default initial lang
    init: [
      {
        title: 'Default title',
        titleTemplate: '%s | My Site',
        htmlAttrs: { lang: 'fr' }
      },
    ]
  })
  const html = `<!-- your html -->`
  return { html, head }
}
```

This counter creates one entry, then patches it on each click:

```ts [counter.ts]
import { useHead } from 'unhead'

export function setupCounter(element: HTMLButtonElement) {
  let counter = 0
  const entry = useHead(window.__UNHEAD__, {})
  const setCounter = (count: number) => {
    counter = count
    element.textContent = `count is ${counter}`
    entry.patch({
      title: counter ? `count is ${counter}` : null,
    })
  }
  element.addEventListener('click', () => setCounter(counter + 1))
  setCounter(0)
}
```

## Next Steps

1. Learn more about app context in the [Wrapping Composables](/docs/typescript/head/guides/core-concepts/wrapping-composables) guide
2. Consider using the [Vite plugin](/docs/head/guides/build-plugins/overview)
