TypeScript

Fix shadcn CLI wrong component paths with extended tsconfig

Why npx shadcn add creates a literal @renderer folder or writes outside your project in electron-vite and WXT apps, and three fixes reproduced on shadcn 4.22.1.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

npx shadcn add decides where to write a component by matching your components.json aliases. On the tsconfig side it reads the paths of one file: the tsconfig.json in your project root (current versions also accept imports in package.json). If your tsconfig aliases live in another file, it gets them wrong. In an electron-vite project (root tsconfig.json with only references), shadcn 2.1.6 created a folder literally called @renderer/, and shadcn 4.22.1 stops with Could not resolve the following aliases. In a WXT project (paths inherited from .wxt/tsconfig.json), both versions write to ../src/, outside your project. Fix it by pointing the CLI at the right config for the one command: TS_NODE_PROJECT=tsconfig.web.json for electron-vite, TS_NODE_BASEURL=.wxt for WXT. For electron-vite you can also copy the paths into the root tsconfig.json once.

What you see#

The bug is shadcn-ui/ui issue #6020, reported on shadcn 2.1.6 with Node.js 22.12.0, 25 reactions, and closed a year later by the stale bot rather than by a fix. Both of its layouts were reproduced in a throwaway node:22.12.0-alpine container on 21 October 2026, against shadcn 2.1.6 and against 4.22.1, the npm latest tag that day.

On the electron-vite layout, the root tsconfig.json holds no compiler options at all:

json
{
  "files": [],
  "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
}

The alias lives in tsconfig.web.json as "@renderer/*": ["src/renderer/src/*"], and components.json uses "components": "@renderer/components". With shadcn 2.1.6 the command succeeds and creates the wrong folder:

text
$ npx shadcn@2.1.6 add button --yes
✔ Checking registry.
✔ Installing dependencies.
✔ Created 1 file:
  - @renderer/components/ui/button.tsx
 
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/electron/@renderer/components/ui/button.tsx

With shadcn 4.22.1 the same project now fails loudly instead, which is the message most people paste into a search box today:

text
$ npx shadcn@latest add button --yes
 
 
Something went wrong. Please check the error below for more details.
If the problem persists, please open an issue on GitHub.
 
Could not resolve the following aliases in /work/electron: components, utils.
Configure path aliases in tsconfig.json or imports in package.json for this workspace and try again.
 
You can also try a previous version to see if that works:
npx shadcn@4.21.0 add button --yes

The suggestion to try a previous version does not help here: the alias is unreadable to every version, and 2.1.6 only "worked" by writing to the wrong place.

The WXT layout fails silently on both versions. Its root tsconfig.json extends ./.wxt/tsconfig.json, which maps "@/*": ["../src/*"]. The project lives in /work/wxt, and the component lands in /work/src:

text
$ npx shadcn@latest add button --yes
✔ Checking registry.
✔ Installing dependencies.
✔ Created 1 file:
  - ../src/components/ui/button.tsx
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/src/components/ui/button.tsx

Why the CLI disagrees with TypeScript#

The CLI does not ask TypeScript where an alias points. In the current source, resolveConfigPaths in packages/registry/src/get-config.ts calls loadConfig(cwd) from the tsconfig-paths package, then resolves each components.json alias. An alias starting with # is looked up in the imports of package.json, then workspace package exports are tried, and only then the paths that tsconfig-paths returned. If an alias such as components or utils does not resolve, assertResolvedAliases throws the error above. Without their own entries in components.json, ui, lib and hooks are derived from the components and utils results, which is why the message lists only components, utils. Two behaviours of that loader explain both layouts.

It never follows references. loadConfig reads the tsconfig.json it finds and merges whatever that file extends. Project references are a different mechanism, and the loader ignores them. A solution-style root file such as electron-vite's has no paths at all, so @renderer/components matches nothing.

It resolves inherited paths from the wrong folder. In tsconfig-paths' config-loader.ts, the base directory for every path is the folder of the root tsconfig.json plus baseUrl, or that folder alone when there is no baseUrl. TypeScript does something else. Its reference for paths says the entries are resolved "relative to the baseUrl if set, or to the tsconfig file itself otherwise". For WXT, "the tsconfig file itself" is .wxt/tsconfig.json, so ../src/* means the project's src. The CLI resolves the same string from the project root and goes one folder too high.

The same container shows that TypeScript itself is happy with the WXT layout. A file importing @/lib/utils, checked with tsc 7.0.2:

text
======== Resolving module '@/lib/utils' from '/work/wxt/src/entry.ts'. ========
'paths' option is specified, looking for a pattern to match module name '@/lib/utils'.
Module name '@/lib/utils', matched pattern '@/*'.
======== Module name '@/lib/utils' was successfully resolved to '/work/wxt/src/lib/utils.ts'. ========

So your editor and tsc agree on the project's own src, and only the CLI that writes the files is wrong. That is the same pattern as tsconfig paths not working in Next.js, where Jest and ESLint each need to be told about an alias separately: paths is a promise to the type checker, and every other tool reads it with its own resolver. The shadcn CLI is one more of those resolvers.

Fix: point the CLI at the config that owns the aliases#

All three fixes below were run on shadcn 4.22.1 in the same container. Each changes one thing, and each puts button.tsx where TypeScript expects it.

1. electron-vite: read tsconfig.web.json for this command#

tsconfig-paths reads two environment variables, both documented in its README: TS_NODE_PROJECT chooses which tsconfig to load, and TS_NODE_BASEURL overrides baseUrl. Because the shadcn CLI calls that loader, the variables reach it too:

bash
TS_NODE_PROJECT=tsconfig.web.json npx shadcn@latest add button --yes
text
✔ Created 1 file:
  - src/renderer/src/components/ui/button.tsx
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/electron/src/renderer/src/components/ui/button.tsx

No file changes. The cost is remembering the prefix on every add, which a package.json script can do for you. In the issue, tsconfig.web.json also extends @electron-toolkit/tsconfig/tsconfig.web.json; that package (version 2.0.0) sets no baseUrl and no paths, so the extra layer does not change the result.

2. electron-vite, permanent: repeat the alias in the root tsconfig.json#

Add a compilerOptions.paths block to the root file, next to files and references:

json
{
  "files": [],
  "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
  "compilerOptions": {
    "paths": {
      "@renderer/*": ["./src/renderer/src/*"]
    }
  }
}

Then run the plain command:

bash
npx shadcn@latest add button --yes
text
✔ Created 1 file:
  - src/renderer/src/components/ui/button.tsx

The root file still compiles nothing ("files": []), so the block only feeds tools that read the root config. Note the ./ prefix and the missing baseUrl: since TypeScript 4.1, the baseUrl reference says it "is no longer required to be set when using paths", and without it the loader resolves the entry from the root folder, which is what you want here. Keep the two copies of the alias in step if you ever rename it.

3. WXT: move the base directory into .wxt#

Do not copy the alias into WXT's root tsconfig.json. The WXT TypeScript guide is explicit: "To add your own, DO NOT add them to your tsconfig.json!" A paths block in the root file replaces the generated one, including ~, @@ and ~~. Instead, tell the loader to resolve from the folder that owns the paths:

bash
TS_NODE_BASEURL=.wxt npx shadcn@latest add button --yes
text
✔ Created 1 file:
  - src/components/ui/button.tsx
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/wxt/src/components/ui/button.tsx

With .wxt as the base, ../src/* lands in the project's own src, the same place tsc resolved above. Your tsconfig.json and the generated file stay untouched, so wxt prepare cannot undo the fix.

Verify the fix#

Run the add command and check the path in the Created 1 file line before you commit anything. It should start inside your source folder (src/renderer/src/ or src/), never with @ or ../. Then search for strays from earlier attempts:

bash
find . -name button.tsx -not -path '*/node_modules/*'
find .. -maxdepth 3 -name button.tsx -not -path '*/node_modules/*'

If the second command finds a button.tsx in the parent folder, or the first finds one under a directory named @renderer, delete those copies. They were written by the failing runs and nothing imports them. Finally, run npx tsc --noEmit (or npx tsc -b for a project with references) so the type checker confirms that the @renderer/lib/utils or @/lib/utils import inside the new component resolves. If you later hit Cannot find module for an alias at runtime, not at type-check time, that is a different problem: tsc does not rewrite aliases in its output, as explained in Cannot find module defined in tsconfig paths.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Shared base configs that use ${configDir}#

Monorepos often keep paths in a shared base file and write them as ${configDir}/src/*, a template variable TypeScript 5.5 added so that the path resolves from each extending project. tsconfig-paths does not substitute it. With this base file extended by the root tsconfig.json, shadcn 4.22.1 creates a folder named after the variable itself:

text
$ npx shadcn@latest add button --yes
✔ Checking registry.
✔ Installing dependencies.
✔ Created 1 file:
  - ${configDir}/src/components/ui/button.tsx
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/configdir/${configDir}/src/components/ui/button.tsx

Moving the base directory cannot help here, because the literal string is the problem, not the folder it is resolved from. What works is the move from fix 2: repeat the alias in the project's own tsconfig.json with a plain relative target. A paths block in the extending file replaces the inherited one, so the CLI never sees the template variable:

json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
text
✔ Created 1 file:
  - src/components/ui/button.tsx
$ find /work -name button.tsx -not -path '*/node_modules/*'
/work/configdir/src/components/ui/button.tsx

Because the override replaces every inherited alias, list all the aliases the project uses, not only the one shadcn needs. A fix is proposed upstream in pull request #10448, and the references case in pull request #9632. Both were open and unmerged on 21 October 2026, so do not expect a newer CLI to make the workarounds unnecessary until one of them ships in a release.

Where the next error usually comes from#

Once the component lands in the right folder, an error that remains is no longer about paths. Two shadcn errors covered on this site come from Tailwind CSS instead. If init stopped earlier with "No Tailwind CSS configuration found", the CLI is looking for an @import "tailwindcss" line or the @tailwind directives, as covered in the shadcn Tailwind check in Vite. If the build then fails on @apply border-border, the shadcn stylesheet is running under Tailwind v4 without its colour config, and Cannot apply unknown utility class border-border walks through loading it with @config or moving it into @theme. Both start from a component that is already where TypeScript expects it, which is the part this page fixes.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote