@happydesigns/nuxt-variants

Composable page capabilities for Nuxt with typed config inheritance and Nuxt Content schema merging.
323 1
Install
npm install @happydesigns/nuxt-variants
Latest Version:0.2.0
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: GitHub  ·  npm
Maintainer: janfr

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

Related Packages

@nuxt-themes/typography

A beautiful typography with a set of prose components to use with Nuxt Content.

73,021
@privyid/nuxt-csrf

Module CSRF for NUXT

26,088 9
mixpanel-nuxt

Mixpanel module for Nuxt

19,992 7
nuxt-radash

radash for Nuxt

10,601 20
gsap-nuxt-module

GSAP integration for Nuxt.

8,932 37

Version History

Version Released Vue License
0.2.0 ^3.0 MIT
0.1.1 ^3.0 MIT
0.1.0 ^3.0 MIT
0.0.6 ^3.0 MIT
0.0.5 ^3.0 MIT