Most "the layer works in its playground but not in my app" reports come down to one sentence from the docs: global aliases such as ~/ and @/ "are resolved relative to the user's project paths". A layer is merged into somebody else's project, so every path it writes must be anchored to itself. This page is the habit that prevents the whole class of bugs.
~, @, ~~, @@ point at the consuming project, both in nuxt.config.ts and inside a layer's runtime code. In the layer's own playground they happen to resolve to the right place, which is why the bug only appears at the first consumer.nuxt.config.ts, anchor to the file:import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = dirname(fileURLToPath(import.meta.url))
export default defineNuxtConfig({
css: [join(currentDir, './app/assets/css/main.css')],
nitro: { storage: { cache: { driver: 'fs', base: join(currentDir, '.cache') } } },
})
layers/base/nuxt.config.ts in this repository does.~/composables/useTeamAuth looks for it in the project. Use .//../ paths, rely on auto-imports (the layer's app/composables is scanned anyway), or use the named alias.#layers/<name> is the by-path escape hatch. Auto-scanned layers are named after their folder; other layers need $meta: { name }. import { formatDuration } from '#layers/base/app/utils/format' resolves regardless of where the consuming project lives, and it is how a project can wrap a layer component it overrides instead of copying it..env is loaded. c12 reads .env from the project root during nuxt dev/build; a .env inside a layer is ignored, and no .env is read by the built server at all. Layer runtimeConfig defaults must therefore be non-secret literal values, with each app supplying NUXT_* variables in its own environment.node_modules, so classes used inside a layer under layers/ are found, while a layer consumed from npm is invisible until the CSS entry says @source "../../../node_modules/@team/nuxt-layer-base"; (three levels up from app/assets/css/main.css to the project root)..nuxt/tsconfig.*.json with paths for every layer, so layer code type-checks inside any consumer and inside the playground; a layer package's own tsconfig.json should just extends the playground's generated one.The wrap-and-override pattern, which needs the alias:
<script setup lang="ts">
// The project overrides the layer's TeamButton by path, but still renders the original inside.
import BaseTeamButton from '#layers/base/app/components/Team/Button.vue'
</script>
<template>
<BaseTeamButton v-bind="$attrs" class="tracking-wide">
<slot />
</BaseTeamButton>
</template>
In a module that ships with the layer, use kit's resolver instead of hand-rolled URL code:
import { addPlugin, createResolver, defineNuxtModule } from 'nuxt/kit'
export default defineNuxtModule({
meta: { name: 'team-devtools' },
setup() {
const { resolve } = createResolver(import.meta.url)
addPlugin(resolve('./runtime/plugin.client')) // anchored to the module file, not the project
},
})
The playground lives inside the layer repository, so ~/assets/css/main.css resolves to the layer's own app/assets. Consumers are elsewhere. Add a CI job that builds a fixture outside the layer folder (or a consumer in another workspace package) so alias mistakes fail before release; see Testing and CI.
@import "~/assets/css/tokens.css" inside a layer stylesheet has the same problem. Use a relative import (@import "./tokens.css") inside the layer's CSS.
The minimum Nuxt version for the #layers/<name> alias (documented as 3.16) and whether $meta.name is required for layers listed in extends.
css: ['~/assets/css/main.css'], extend it from an app in a different directory and read the error. Fix it with import.meta.url.#layers/<name>/app/components/.... Confirm both the layer's markup and your wrapper appear..env with NUXT_PUBLIC_API_BASE inside the layer, then in the project. Log useRuntimeConfig().public.apiBase in both cases."Paths. Aliases like ~ and @ resolve against the consuming project, so a layer that references its own CSS or assets through them works in its playground and breaks in the first real app. Inside the layer's nuxt.config I anchor everything to import.meta.url; in runtime code I use relative imports, auto-imports, or the #layers/<name> alias when a project needs to import a layer file by path. The same rule explains two neighbours: only the project's .env is loaded, and Tailwind only scans the project tree, so an npm-installed layer needs an explicit @source."
Priority and merging
Which layer wins when two define the same file or option, how c12 and defu merge configs, and how to prove the order instead of memorising it.
Config merging in practice
How modules, runtimeConfig, app.config, route rules, Vite options and environment overrides behave when a layer and a project both declare them.