Unhead: one head and template manager across frameworks
The full stack <head> package for any framework.
At a glance
- What is it?
- unhead is a MIT licensed TypeScript package that manages the document head for Vue, React, Solid, Svelte and Angular apps, with deduplication, Capo.js tag ordering, streaming SSR and a Vite plugin for Nuxt-style SSR work.
- Who is it for?
- Unhead is a good fit for teams rendering many pages on the server where head tags differ per route, and a poor fit for a static site with a single hand written head. Verify first that the framework adapter you need is listed in the repository examples and that your build tool can run the bundled Vite plugin.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem unhead solves inside an SSR app
A server rendered page usually has a head that changes per route: the title follows the page, the description follows the record, the canonical URL follows the URL. Managing those tags in a framework specific way means learning one API per framework, and in a large application means the same rules written twice. unhead is a single package that takes that job, described by the README as a document head and template manager that improves reactive SSR framework developer experience, SEO and performance.
The package is written in TypeScript, published under MIT, and the repository is a pnpm monorepo with the packages under packages/, examples for each supported framework, and a bench directory dedicated to bundle size. The README lists Vue, React, Solid, Svelte and Angular as supported and names Nuxt as a user of the package. So the audience is a frontend team running server rendering where the head is built at request time, and the pitch is a shared vocabulary across frameworks rather than a single framework plugin.
The repository also carries a unified Vite plugin with DevTools, tree shaking, validation and source tracing. That tooling matters more than it first appears, because a head manager that pulls every tag builder into a client bundle is a cost on pages that need none of it.
Installing unhead and the package manager it expects
The install is a single npm command, and the package is published on npm as unhead:
npm install unheadThe repository itself is a pnpm workspace, and the root manifest pins the package manager, which tells you what the maintainers use for development and for the release build:
{
"name": "unhead-monorepo",
"type": "module",
"version": "3.4.3",
"private": true,
"packageManager": "[email protected]",
"author": "Harlan Wilton <[email protected]>",
"license": "MIT",
"homepage": "https://unhead.unjs.io"
}Only the root manifest is private. The published package is separate, and the deep guides live on the documentation site rather than in the repository, so a reader who wants the API detail has to leave the checkout. The README also points to a separate tool, skilld, for generating an agent skill for the package:
npx skilld add unheadThat command is a convenience for tooling, not a build step, and the install of the library itself remains the one npm line above.
useHead, useSeoMeta and useScript are the three entry points
Three composables carry most of the work, and the README summarises what each one does. useHead() handles deduplication, sorting, tag merging and type narrowing, so two components can both declare a title and the framework decides how they combine rather than emitting two title tags. useSeoMeta() is the flatter alternative, described as flat SEO meta tags with a compile-time transform, which means the transform happens during the build rather than at runtime. useScript() is the script loading API, described in the README as performant and built around loading behaviour rather than a plain script tag.
The type narrowing claim is the part with a real cost and a real benefit. Head entries are typed, so a mistyped attribute or a wrong tag name is a type error at build time, and the README ties the compile-time transform to that. For a team that is a net gain. For a project that renders head data from a CMS with loose typing, the types become something to cast around, and the repository does not document a runtime validation path for untyped input beyond the validation the Vite plugin performs at build time.
The unit of work is the tag, not the head. That is what makes deduplication and Capo.js ordering possible, and it also means the same declarations work identically on the server and in the browser, which matters for streaming output.
Capo.js ordering and the size of the client payload
unhead sorts tags, and the sorting follows Capo.js, the algorithm that orders head elements by how likely they are to be needed first. Title, meta charset and the description come early, and things that slow rendering, such as a large stylesheet or a late script, come later. For a server rendered page this is a rendering order decision rather than a cosmetic one, because the browser cannot paint what it has not parsed.
The size claim in the README is specific: 4.4kb gz, tree shaken. That figure refers to the base package, and the README also names streaming SSR as a v3 feature, where head tags push to the DOM as suspense boundaries resolve, so the head is not held back waiting for the whole page. Whether the 4.4kb figure applies to your build depends on what you import, which is the reason the Vite plugin advertises tree shaking and source tracing: the plugin is meant to show you which tags ended up in which chunk.
The repository backs this with dedicated scripts. The root package.json carries a test:bundle-size script that builds several client and server bundles through unbuild, plus separate scripts for the Vue, React and schema.org bundle sizes.
"test:bundle-size": "unbuild bench/bundle --config bench/bundle/client-build.config.ts && unbuild bench/bundle --config bench/bundle/client-full-build.config.ts && unbuild bench/bundle --config bench/bundle/client-sc-build.config.ts && unbuild bench/bundle --config bench/bundle/server-build.config.ts && unbuild bench/bundle --config bench/bundle/server-sc-build.config.ts",The presence of separate schema.org and full-client configurations suggests the maintainers treat tag size as a maintained budget rather than an afterthought.
Streaming SSR and what v3 changed
The README calls out v3 with a headline of 32% faster, 11% smaller, streaming SSR and a unified Vite plugin with DevTools, and points to release notes hosted on the documentation site. Those numbers come from the project's own announcement, so treat them as a claim about the comparison the maintainers ran rather than a general measurement.
Streaming SSR is the structural change. In a traditional server render, the head is assembled first, the whole document is serialised, and the client picks up from there. With suspense boundaries resolving independently, unhead can push head tags to the DOM as those boundaries resolve, so a page does not wait for its slowest subtree before the browser receives metadata. The examples directory makes the scope concrete: there are separate example projects for vite-ssr-react-streaming, vite-ssr-solidjs-streaming, vite-ssr-svelte-streaming, vite-ssr-vue-streaming, alongside the non streaming variants and a vite-ssr-vue-prerender example.
The practical consequence is that streaming changes when the head is final. If a tag depends on data that only resolves later, the ordering guarantee people expect from a single synchronous head assembly is weaker, and a project that assumes the head is complete in the first flush will have to be checked against the streaming examples in the repository.
Where unhead is the wrong tool
The clearest case against unhead is a project that does not render on the server. The package targets reactive SSR, and its features are aimed at the server render path: streaming, dedup across a render, compile-time transforms. A static site with one hand written head file has nothing to deduplicate and nothing to stream, and would carry the dependency for nothing.
The second limit is knowledge of the head surface. The README is short and points to the documentation site for guides and API references, and the repository listing shows docs/ alongside the packages, examples and bench directories. The one thing the README does not spell out is how each framework adapter is wired into an application, so the first hour with unhead in a given framework is reading the docs site and the matching example project rather than reading a README section.
The third limit is bundle discipline. Deduplication only works if declarations are typed and consistently written. A codebase that spreads head declarations across legacy string templates will not get the merge behaviour, and the transformation rules for a compile-time transform are a place where behaviour can differ between build and production bundles if the plugin is not applied to every entry point.
unhead against framework native head management
Every framework in the supported list already ships something. React has the react-helmet family of packages, Svelte has its own head elements, and Angular has the Meta and Title services. The difference in approach is that the native options are one per framework, tied to that framework's reactivity, and generally give you a tag or two without deciding where the tag belongs in the document order.
unhead instead treats the head as a document to be assembled, with a data model of tags, a merge and dedup policy across components, and an explicit ordering algorithm in Capo.js. A single page can contribute a title from a layout, a description from a data loader and an og image from a component, and the manager decides the outcome. Framework native tools leave the ordering of the head to the order in which your code happens to run, which is exactly the problem that shows up when several layers of a page want to set the same tag.
The cost of the shared approach is a dependency and a learning curve, plus the requirement that the build pipeline can apply the Vite plugin. For a team maintaining apps in two of the supported frameworks, the argument is about writing the head rules once. For a team with one framework and one page shape, the native option is less machinery for the same result.
Editorial conclusion
Unhead is a good fit for teams rendering many pages on the server where head tags differ per route, and a poor fit for a static site with a single hand written head. Verify first that the framework adapter you need is listed in the repository examples and that your build tool can run the bundled Vite plugin.
Frequently asked questions
How do I install unhead?
Run npm install unhead in your project. The package is published on npm under that name and works with Vue, React, Solid, Svelte and Angular, with the full guides hosted on the documentation site rather than in the repository README.
Which frameworks does unhead support?
The README lists Vue, React, Solid, Svelte and Angular, and names Nuxt as a user of the package. The repository examples directory contains a separate example project for each of those frameworks, including streaming variants for React, Solid, Svelte and Vue.
What is the difference between useHead and useSeoMeta?
useHead is the general purpose composable, handling deduplication, sorting, tag merging and type narrowing. useSeoMeta is the flatter option for SEO meta tags, and the README describes it as having a compile-time transform.
How does unhead order the tags in the head?
It sorts them using the Capo.js algorithm, which places elements that are needed for rendering earlier and defers ones that can slow rendering. The README also states the package is 4.4kb gz and tree shaken.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unjs-unhead)Was this useful?