114k

Package Imports

Configure shadcn/ui with package.json imports.

The shadcn CLI supports package imports for installing components, rewriting imports, and resolving third-party registries.

Package imports let you use private #... import aliases from your package.json instead of compilerOptions.paths in tsconfig.json.

Example

You configure imports in your package.json:

package.json
{
  "imports": {
    "#components/*": "./src/components/*.tsx",
    "#lib/*": "./src/lib/*.ts",
    "#hooks/*": "./src/hooks/*.ts"
  }
}

Then import generated components using #... specifiers:

import { Button } from "#components/ui/button"
import { cn } from "#lib/utils"

App

For Next.js, Vite, and TanStack Start apps that install components into the same workspace.

Configure package.json

Add imports for the shadcn/ui install targets.

package.json
{
  "imports": {
    "#components/*": "./src/components/*.tsx",
    "#lib/*": "./src/lib/*.ts",
    "#hooks/*": "./src/hooks/*.ts"
  }
}

If your app does not use a src directory, remove src/ from the targets. For example:

package.json
{
  "imports": {
    "#components/*": "./components/*.tsx",
    "#lib/*": "./lib/*.ts",
    "#hooks/*": "./hooks/*.ts"
  }
}

Configure TypeScript

Enable package import resolution.

tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "resolvePackageJsonImports": true
  }
}

You do not need compilerOptions.paths for these aliases.

Configure components.json

Use the same #... roots in components.json.

components.json
{
  "aliases": {
    "components": "#components",
    "ui": "#components/ui",
    "lib": "#lib",
    "hooks": "#hooks",
    "utils": "#lib/utils"
  }
}

The ui alias uses #components/ui. It is still covered by the #components/* import in package.json.

The utils alias uses #lib/utils. It is covered by #lib/*, so you do not need a separate #utils import.

Monorepo

In a monorepo, use package imports for files inside each package and package exports for files shared across workspaces.

For an app workspace:

apps/web/package.json
{
  "name": "web",
  "private": true,
  "imports": {
    "#components/*": "./src/components/*.tsx",
    "#lib/*": "./src/lib/*.ts",
    "#hooks/*": "./src/hooks/*.ts"
  },
  "dependencies": {
    "@workspace/ui": "workspace:*"
  }
}
apps/web/components.json
{
  "aliases": {
    "components": "#components",
    "ui": "@workspace/ui/components",
    "lib": "#lib",
    "hooks": "#hooks",
    "utils": "@workspace/ui/lib/utils"
  }
}

For the shared UI package:

packages/ui/package.json
{
  "name": "@workspace/ui",
  "private": true,
  "imports": {
    "#components/*": "./src/components/*.tsx",
    "#lib/*": "./src/lib/*.ts",
    "#hooks/*": "./src/hooks/*.ts"
  },
  "exports": {
    "./globals.css": "./src/styles/globals.css",
    "./components/*": "./src/components/*.tsx",
    "./lib/*": "./src/lib/*.ts",
    "./hooks/*": "./src/hooks/*.ts"
  }
}
packages/ui/components.json
{
  "aliases": {
    "components": "#components",
    "ui": "#components",
    "lib": "#lib",
    "hooks": "#hooks",
    "utils": "#lib/utils"
  }
}

When you run add from apps/web, app-local files use #... imports and shared UI files are imported from @workspace/ui.

import { Button } from "@workspace/ui/components/button"
import { LoginForm } from "#components/login-form"

File extensions

The target pattern in package.json#imports controls whether generated imports include file extensions.

package.json
{
  "imports": {
    "#components/*": "./src/components/*.tsx"
  }
}

This generates imports without extensions:

import { Button } from "#components/ui/button"

If you use a target without the extension:

package.json
{
  "imports": {
    "#components/*": "./src/components/*"
  }
}

The generated import keeps the source extension:

import { Button } from "#components/ui/button.tsx"

For most apps, use the extension in the target pattern.

Troubleshooting

If TypeScript cannot resolve a #... import, check that:

  • the specifier starts with #
  • the imports entry is in the nearest package.json
  • moduleResolution is set to bundler
  • resolvePackageJsonImports is enabled
  • the matching target exists after components are added

If a component is installed but imports still point to @/..., check that components.json uses the same #... aliases as your package imports.