npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@happydesigns/nuxt-variants

v0.2.0

Published

Composable page capabilities for Nuxt with typed config inheritance and Nuxt Content schema merging.

Readme

Nuxt Variants

npm version npm downloads License Nuxt

Nuxt Variants is a small Nuxt module for building one shared layout that can behave differently per page. Define reusable feature configs, compose them into named page variants, and resolve the merged result in your layout with useVariant.

The package name is @happydesigns/nuxt-variants.

Why Use It?

Nuxt lets a page select a layout, but it does not describe which capabilities a page type needs inside that layout. Consider a content site with articles, events, and regular pages:

  • all three use the same content shell;
  • articles and events show a header, table of contents, copy action, and previous/next navigation;
  • articles have authors, while events have a location;
  • a short contact or overview page can use the same layout without a table of contents.

Without a shared model, this usually becomes duplicated layouts, checks such as collection === "article", and separately maintained Nuxt Content schemas. Nuxt Variants lets the app name small capabilities such as header, toc, authors, location, copyButton, and surround, then compose them into article, event, and content variants.

This is useful when several page or collection types share a layout but not all of its behavior. A small app with one layout and a few static props usually does not need a variant graph.

Nuxt Variants separates structural decisions from configurable values:

  • nuxt.config.ts defines every name and inheritance edge, plus optional code-owned defaults.
  • app.config.ts supplies layer or application defaults and runtime overrides for registered names.
  • Pages select a variant with definePageMeta.
  • Layouts call useVariant and render from the resolved config.
  • Nuxt Content can merge schemas from the same variant inheritance graph.

Features

  • Flat variant registry for both reusable features and page variants.
  • Deep object merge with array replacement, not array concatenation.
  • extends inheritance across direct and transitive parents.
  • Reactive app.config overrides with generated types from both config sources.
  • Auto-generated TypeScript types through #nuxt-variants.
  • Build-time virtual graph through #variants-graph.
  • Fail-fast diagnostics for unknown parents, inheritance cycles, and invalid runtime structure.
  • Nuxt DevTools inspector with filtering, resolution order, layer provenance, and resolved output.
  • Graph-aware Nuxt Content schema helper through @happydesigns/nuxt-variants/schemas.

Quick Setup

Use Node.js 22.19 or newer on the 22.x release line, Node.js 24.11 or newer on the 24.x release line, or Node.js 26+.

npx nuxt module add @happydesigns/nuxt-variants

Manual install:

pnpm:

pnpm add @happydesigns/nuxt-variants

npm:

npm install @happydesigns/nuxt-variants

yarn:

yarn add @happydesigns/nuxt-variants

bun:

bun add @happydesigns/nuxt-variants

Then register the module:

export default defineNuxtConfig({
  modules: ["@happydesigns/nuxt-variants"],
});

Basic Usage

1. Define Variants

export default defineNuxtConfig({
  modules: ["@happydesigns/nuxt-variants"],
  variants: {
    registry: {
      dates: {},
      authors: {},
      location: {},
      header: {},
      toc: {},
      copyButton: {},
      surround: {},
      article: {
        extends: ["dates", "authors", "header", "toc", "copyButton", "surround"],
        config: {},
      },
      event: {
        extends: ["dates", "location", "header", "toc", "copyButton", "surround"],
        config: {},
      },
      content: ["header", "toc"],
    },
  },
});

2. Override At Runtime

export default defineAppConfig({
  variants: {
    copyButton: {
      config: {
        copyButton: {
          label: "Copy URL",
          successLabel: "Link copied",
        },
      },
    },
  },
});

app.config.ts wins over nuxt.config.ts for the same registered variant. Generated config types include both the registry and Nuxt's merged AppConfig, so an entry may be structural in the registry while its complete value contract lives in a layer or application app config. Names and extends remain in the registry; changing structure at runtime would make generated types, Content schemas, and rendering disagree, so Nuxt Variants rejects it during startup.

Current Nuxt Studio versions edit Nuxt Content files rather than app.config.ts directly. For owner-editable settings, define a small app-owned data collection with an explicit schema and map its values to updateAppConfig. This preserves the reactive variant API without exposing the technical variant graph to editors. See the documentation example for the complete pattern.

3. Select A Variant Per Page

definePageMeta({
  layout: "content",
  variant: "article",
});

4. Resolve The Variant In A Layout

This mirrors the shared content layout in @happydesigns/ui. Nuxt Variants provides useVariant; the rendered Nuxt UI and H* components remain owned by the application or UI layer.

<template>
  <UPage>
    <UPageHeader v-if="hasHeader" />

    <UPageBody>
      <slot />
      <HCopyButton v-if="hasCopyButton" v-bind="config.copyButton" />
      <HSurround v-if="hasSurround" />
    </UPageBody>

    <template v-if="hasToc" #right>
      <UContentToc />
    </template>
  </UPage>
</template>

<script setup lang="ts">
const route = useRoute();
const variantName = computed(() => route.meta.variant ?? "article");

const { config, has } = useVariant(variantName);
const hasHeader = has("header");
const hasToc = has("toc");
const hasCopyButton = has("copyButton");
const hasSurround = has("surround");
</script>

When the variant name is a literal, config is typed from the build-time registry and Nuxt's merged AppConfig:

const { config, has } = useVariant("article");

config.value.copyButton;

has("authors").value; // true
has("location").value; // false

Nuxt Content

Nuxt Variants ships mergeVariantSchemas for Nuxt Content v3. It walks the variant graph and produces one Zod or Valibot object schema with inherited fields included.

Zod and Valibot are optional peer dependencies. Install the validator used by your Content schema; the other validator is not loaded or required.

Keep the registry in a normal TypeScript file when both Nuxt and Nuxt Content need it. This avoids module-order and virtual-alias coupling.

// variants.ts
import { defineVariantRegistry } from "@happydesigns/nuxt-variants/schemas";

export const variantRegistry = defineVariantRegistry({
  dates: {},
  authors: {},
  header: {},
  toc: {},
  article: { extends: ["dates", "authors", "header", "toc"] },
});
// nuxt.config.ts
import { variantRegistry } from "./variants";

export default defineNuxtConfig({
  modules: ["@happydesigns/nuxt-variants", "@nuxt/content"],
  variants: { registry: variantRegistry },
});
// content.config.ts
import { defineCollection, property } from "@nuxt/content";
import { z } from "zod";
import { createVariantSchemaResolver } from "@happydesigns/nuxt-variants/schemas";
import { variantRegistry } from "./variants";

const variantSchemas = {
  dates: z.object({ date: z.date().optional() }),
  authors: z.object({ authors: z.array(z.string()).optional() }),
  header: z.object({
    header: property(z.object({})).inherit("@nuxt/ui/components/PageHeader.vue").optional(),
  }),
  toc: z.object({ toc: z.boolean().default(true) }),
};
const resolveVariantSchema = createVariantSchemaResolver(variantRegistry, variantSchemas);

export const collections = {
  blog: defineCollection({
    type: "page",
    source: "blog/**",
    schema: resolveVariantSchema(["article"]),
  }),
};

The resolver builds the explicit graph once from the shared registry and reuses it for every collection. Unknown active variants and schema registry keys throw immediately instead of producing an incomplete collection schema.

TypeScript

The module generates CustomVariantRegistry, VariantName, VariantNameInput, and VariantConfigOf in #nuxt-variants during Nuxt prepare. VariantNameInput retains suggestions for known names while accepting dynamic route or CMS values.

import type { VariantConfigOf } from "#nuxt-variants";

type ArticleConfig = VariantConfigOf<"article">;

Generated config values are widened to primitive types. Config declared only in app.config.ts is included automatically. If ordinary inference cannot express a deliberately narrower or library-owned type, augment CustomVariantOverrides in a module declaration:

import type { ButtonProps } from "@nuxt/ui";

export {};

declare module "#nuxt-variants" {
  interface CustomVariantOverrides {
    backButton: {
      backButton: Pick<ButtonProps, "icon" | "label" | "to">;
    };
  }
}

An override replaces the inferred config type for that registry entry and is also applied when article or another variant inherits the entry. It changes TypeScript types only; runtime values still come from nuxt.config.ts and app.config.ts.

API Overview

useVariant(name) returns { config, features, has }.

  • config is a ComputedRef of the fully merged config.
  • features is a ComputedRef<ReadonlySet<string>> resolved once per reactive change.
  • has(featureName) returns a ComputedRef<boolean> when the selected variant is that feature or inherits it directly or transitively.
  • name and featureName can be strings, refs, computed refs, or getters.
  • active: false disables both resolved config and has() checks for that variant.

useVariants() returns a computed list of known variants:

interface VariantEntry {
  name: VariantName;
  extends: VariantName[];
  configKeys: string[];
}

Virtual modules:

  • #nuxt-variants exposes generated types.
  • #variants-graph exposes variantGraph and variantDiagnostics.

Development tooling:

  • The Nuxt DevTools tab named Nuxt Variants follows the current route variant, filters by variant, parent, config key, or source layer, and shows resolution order, activity, layer provenance, raw inputs, and resolved config. Refreshing the inspector reads the current app.config overrides again.
  • The backing inspector route is registered only in Nuxt dev and test environments.

Merge Rules

For a variant's own config, app.config.ts wins over nuxt.config.ts.

For every registry entry, app.config.ts overrides nuxt.config.ts. Across the inheritance graph, parents are resolved first and the child overrides them. If multiple parents define the same value, the later parent in extends wins. Arrays are replaced:

base: {
  config: { slots: ["header", "main"], color: "blue" },
},
article: {
  extends: ["base"],
  config: { slots: ["article"], density: "comfortable" },
},

Resolving article produces:

{
  slots: ["article"],
  color: "blue",
  density: "comfortable",
}

Diagnostics

During Nuxt prepare, Nuxt Variants stops with one structured VariantRegistryError when the registry contract is invalid:

  • variants extending unknown parent keys
  • circular inheritance chains
  • app.config entries for unknown variants
  • app.config entries that define structural extends
  • unknown fields in registry entries or runtime overrides
  • malformed entries or invalid extends, active, and config value types

The error contains all detected diagnostics with stable codes, so one prepare run can identify every problem. Valid graph data remains available from #variants-graph and in the Nuxt DevTools inspector.

Registry entries accept only extends, active, and config. Runtime app.config overrides accept only active and config; misspelled or extra fields fail during startup instead of being silently ignored.

Playground And Documentation

The repository pins pnpm through packageManager, so Corepack and CI use the same package manager version.

pnpm install
pnpm dev

The playground demonstrates shared layouts, feature composition, runtime overrides, generated type contracts, and Nuxt Content schema merging.

pnpm docs:dev

The Docus documentation source lives in docs/. See CONTRIBUTING.md for branch, commit, and PR rules.

Local Checks

pnpm dev:prepare
pnpm lint
pnpm typecheck
pnpm test
pnpm prepack
pnpm dev:build
pnpm docs:build