diff --git a/.changeset/good-suits-know.md b/.changeset/good-suits-know.md
new file mode 100644
index 0000000000..7107ebd9ab
--- /dev/null
+++ b/.changeset/good-suits-know.md
@@ -0,0 +1,5 @@
+---
+"shadcn": minor
+---
+
+add npx shadcn eject
diff --git a/apps/v4/content/docs/(root)/cli.mdx b/apps/v4/content/docs/(root)/cli.mdx
index dbf7019ad4..aa81bac9e1 100644
--- a/apps/v4/content/docs/(root)/cli.mdx
+++ b/apps/v4/content/docs/(root)/cli.mdx
@@ -3,6 +3,8 @@ title: shadcn
description: Use the shadcn CLI to add components to your project.
---
+import { TriangleAlertIcon } from "lucide-react"
+
## init
Use the `init` command to initialize configuration and dependencies for an existing project, or create a new project with `--name`.
@@ -503,3 +505,87 @@ npx shadcn@latest migrate radix "src/components/ui/**"
If no path is provided, the migration will transform all files in your `ui` directory (from `components.json`).
Once complete, you can remove any unused `@radix-ui/react-*` packages from your `package.json`.
+
+---
+
+## eject
+
+When you run `init`, shadcn adds `@import "shadcn/tailwind.css"` to your global CSS file. This import provides shared Tailwind v4 utilities such as custom variants (`data-open:`, `data-closed:`, etc.) and accordion animations.
+
+Use the `eject` command to inline `shadcn/tailwind.css` into your global CSS file and remove the `shadcn` dependency from your project.
+
+}>
+ **Note: This action is irreversible.** After ejecting, future shadcn CLI
+ updates to `shadcn/tailwind.css` will not apply automatically.
+
+
+```bash
+npx shadcn@latest eject
+```
+
+**Before**
+
+```css
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+```
+
+**After**
+
+```css
+@import "tailwindcss";
+@import "tw-animate-css";
+/* ejected from shadcn@4.8.3 */
+@theme inline {
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(
+ --radix-accordion-content-height,
+ var(--accordion-panel-height, auto)
+ );
+ }
+ }
+}
+
+@custom-variant data-open {
+ &:where([data-state="open"]),
+ &:where([data-open]:not([data-open="false"])) {
+ @slot;
+ }
+}
+
+@utility no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+```
+
+**Monorepo**
+
+In a monorepo, run the command from the workspace that contains your `components.json` and global CSS file:
+
+```bash
+npx shadcn@latest eject -c packages/ui
+```
+
+**Options**
+
+```bash
+Usage: shadcn eject [options]
+
+inline shadcn/tailwind.css and remove the shadcn dependency
+
+Options:
+ -c, --cwd the working directory. defaults to the current directory.
+ -y, --yes skip confirmation prompt. (default: false)
+ -s, --silent mute output. (default: false)
+ -h, --help display help for command
+```
diff --git a/apps/v4/content/docs/changelog/2026-05-shadcn-eject.mdx b/apps/v4/content/docs/changelog/2026-05-shadcn-eject.mdx
new file mode 100644
index 0000000000..4fa8e8fe78
--- /dev/null
+++ b/apps/v4/content/docs/changelog/2026-05-shadcn-eject.mdx
@@ -0,0 +1,61 @@
+---
+title: May 2026 - shadcn eject
+description: Inline shadcn/tailwind.css and remove the shadcn dependency.
+date: 2026-05-31
+---
+
+When we added support for both Radix and Base UI, we needed a place for shared Tailwind utilities that both libraries depend on, e.g. custom variants like `data-open:` and `data-closed:` and utilities like `no-scrollbar`.
+
+We also ran into a few bugs while working on RTL support that were easier to fix in one shared place rather than duplicating across every component.
+
+So we created `shadcn/tailwind.css`. When you run `init`, it adds `@import "shadcn/tailwind.css"` to your global CSS file. It works just like other CSS imports such as `tw-animate-css`: a small dependency that is tree-shaken in production and resolved at build time.
+
+If you prefer not to depend on the `shadcn` package for that CSS, we've added the `shadcn eject` command. It inlines `shadcn/tailwind.css` into your global CSS file and removes the `shadcn` dependency from your project.
+
+```bash
+npx shadcn@latest eject
+```
+
+**Before**
+
+```css
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+```
+
+**After**
+
+```css
+@import "tailwindcss";
+@import "tw-animate-css";
+/* ejected from shadcn@4.8.3 */
+@theme inline {
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(
+ --radix-accordion-content-height,
+ var(--accordion-panel-height, auto)
+ );
+ }
+ }
+}
+
+@custom-variant data-open {
+ &:where([data-state="open"]),
+ &:where([data-open]:not([data-open="false"])) {
+ @slot;
+ }
+}
+```
+
+In a monorepo, run the command from the workspace that contains your `components.json` and global CSS file:
+
+```bash
+npx shadcn@latest eject -c packages/ui
+```
+
+See the [CLI documentation](/docs/cli#eject) for more details.
diff --git a/packages/shadcn/CHANGELOG.md b/packages/shadcn/CHANGELOG.md
index fcc41fd96a..6ed38ace85 100644
--- a/packages/shadcn/CHANGELOG.md
+++ b/packages/shadcn/CHANGELOG.md
@@ -1,4 +1,4 @@
-# @shadcn/ui
+# shadcn
## 4.8.3
diff --git a/packages/shadcn/src/commands/eject.test.ts b/packages/shadcn/src/commands/eject.test.ts
new file mode 100644
index 0000000000..76406c8337
--- /dev/null
+++ b/packages/shadcn/src/commands/eject.test.ts
@@ -0,0 +1,262 @@
+import { mkdir, mkdtemp, readFile, rm, writeFile } from "fs/promises"
+import os from "os"
+import path from "path"
+import {
+ formatMonorepoMessage,
+ getMonorepoTargets,
+ isMonorepoRoot,
+} from "@/src/utils/get-monorepo-info"
+import { getPackageManager } from "@/src/utils/get-package-manager"
+import { execa } from "execa"
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
+
+import { runEject, SHADCN_TAILWIND_IMPORT } from "./eject"
+
+vi.mock("execa", () => ({
+ execa: vi.fn(),
+}))
+
+vi.mock("@/src/utils/get-package-manager", () => ({
+ getPackageManager: vi.fn().mockResolvedValue("pnpm"),
+}))
+
+vi.mock("@/src/utils/get-monorepo-info", async (importOriginal) => {
+ const actual =
+ await importOriginal()
+ return {
+ ...actual,
+ formatMonorepoMessage: vi.fn(),
+ getMonorepoTargets: vi.fn().mockResolvedValue([]),
+ isMonorepoRoot: vi.fn().mockResolvedValue(false),
+ }
+})
+
+vi.mock("@/src/utils/spinner", () => ({
+ spinner: vi.fn(() => ({
+ start: vi.fn().mockReturnThis(),
+ succeed: vi.fn(),
+ })),
+}))
+
+vi.mock("@/src/utils/logger", () => ({
+ logger: {
+ break: vi.fn(),
+ error: vi.fn(),
+ log: vi.fn(),
+ warn: vi.fn(),
+ },
+}))
+
+const fixtureConfig = {
+ $schema: "https://ui.shadcn.com/schema.json",
+ style: "base-nova",
+ rsc: true,
+ tsx: true,
+ tailwind: {
+ config: "",
+ css: "app/globals.css",
+ baseColor: "neutral",
+ cssVariables: true,
+ },
+ aliases: {
+ components: "@/components",
+ utils: "@/lib/utils",
+ ui: "@/components/ui",
+ lib: "@/lib",
+ hooks: "@/hooks",
+ },
+}
+
+const fixtureTsconfig = {
+ compilerOptions: {
+ baseUrl: ".",
+ paths: {
+ "@/*": ["./*"],
+ },
+ },
+}
+
+const baseCss = `@import "tailwindcss";
+@import "tw-animate-css";
+@import "shadcn/tailwind.css";
+
+@layer base {
+ body {
+ @apply bg-background text-foreground;
+ }
+}
+`
+
+describe("runEject", () => {
+ let tempDir: string
+
+ beforeEach(async () => {
+ vi.clearAllMocks()
+ vi.mocked(getPackageManager).mockResolvedValue("pnpm")
+ tempDir = await mkdtemp(path.join(os.tmpdir(), "shadcn-eject-"))
+ })
+
+ afterEach(async () => {
+ await rm(tempDir, { recursive: true, force: true })
+ })
+
+ async function setupProject(
+ cssContent: string,
+ packageJson: Record = {
+ name: "test-app",
+ dependencies: {
+ shadcn: "^4.8.3",
+ },
+ }
+ ) {
+ await writeFile(
+ path.join(tempDir, "components.json"),
+ JSON.stringify(fixtureConfig, null, 2)
+ )
+ await writeFile(
+ path.join(tempDir, "tsconfig.json"),
+ JSON.stringify(fixtureTsconfig, null, 2)
+ )
+ await mkdir(path.join(tempDir, "app"), { recursive: true })
+ await writeFile(path.join(tempDir, "app", "globals.css"), cssContent)
+ await writeFile(
+ path.join(tempDir, "package.json"),
+ JSON.stringify(packageJson, null, 2)
+ )
+ }
+
+ it("inlines shadcn/tailwind.css and removes the dependency", async () => {
+ await setupProject(baseCss)
+
+ await runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+
+ const output = await readFile(
+ path.join(tempDir, "app", "globals.css"),
+ "utf8"
+ )
+ const shadcnCss = await readFile(
+ path.join(process.cwd(), "src/tailwind.css"),
+ "utf8"
+ )
+
+ expect(output).not.toMatch(SHADCN_TAILWIND_IMPORT)
+ expect(output).toContain('@import "tailwindcss";')
+ expect(output).toContain('@import "tw-animate-css";')
+ expect(output).toContain("/* ejected from shadcn@4.8.3 */")
+ expect(output).toContain(shadcnCss.trim())
+ expect(output).toContain("@layer base")
+ expect(output).toContain("@apply bg-background text-foreground;")
+ expect(execa).toHaveBeenCalledWith("pnpm", ["remove", "shadcn"], {
+ cwd: tempDir,
+ })
+ })
+
+ it("removes shadcn from devDependencies", async () => {
+ await setupProject(baseCss, {
+ name: "test-app",
+ devDependencies: {
+ shadcn: "^4.8.3",
+ },
+ })
+
+ await runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+
+ const output = await readFile(
+ path.join(tempDir, "app", "globals.css"),
+ "utf8"
+ )
+
+ expect(output).toContain("/* ejected from shadcn@4.8.3 */")
+ expect(execa).toHaveBeenCalledWith("pnpm", ["remove", "shadcn"], {
+ cwd: tempDir,
+ })
+ })
+
+ it("skips dependency removal when shadcn is not in package.json", async () => {
+ await setupProject(baseCss, {
+ name: "test-app",
+ dependencies: {},
+ })
+
+ await runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+
+ expect(execa).not.toHaveBeenCalled()
+ })
+
+ it("removes shadcn from package.json for deno", async () => {
+ vi.mocked(getPackageManager).mockResolvedValue("deno")
+ await setupProject(baseCss)
+
+ await runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+
+ const packageJson = JSON.parse(
+ await readFile(path.join(tempDir, "package.json"), "utf8")
+ )
+
+ expect(packageJson.dependencies?.shadcn).toBeUndefined()
+ expect(execa).not.toHaveBeenCalled()
+ })
+
+ it("exits when shadcn/tailwind.css is not imported", async () => {
+ await setupProject(`@import "tailwindcss";
+@import "tw-animate-css";
+`)
+
+ const exit = vi.spyOn(process, "exit").mockImplementation((() => {
+ throw new Error("process.exit")
+ }) as never)
+
+ await expect(
+ runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+ ).rejects.toThrow("process.exit")
+
+ exit.mockRestore()
+ })
+
+ it("shows monorepo suggestions when run from a monorepo root", async () => {
+ vi.mocked(isMonorepoRoot).mockResolvedValue(true)
+ vi.mocked(getMonorepoTargets).mockResolvedValue([
+ { name: "packages/ui", hasConfig: true },
+ { name: "apps/web", hasConfig: true },
+ ])
+
+ const exit = vi.spyOn(process, "exit").mockImplementation((() => {
+ throw new Error("process.exit")
+ }) as never)
+
+ await expect(
+ runEject({
+ cwd: tempDir,
+ yes: true,
+ silent: true,
+ })
+ ).rejects.toThrow("process.exit")
+
+ expect(formatMonorepoMessage).toHaveBeenCalledWith("eject", [
+ { name: "packages/ui", hasConfig: true },
+ { name: "apps/web", hasConfig: true },
+ ])
+
+ exit.mockRestore()
+ })
+})
diff --git a/packages/shadcn/src/commands/eject.ts b/packages/shadcn/src/commands/eject.ts
new file mode 100644
index 0000000000..8c17495559
--- /dev/null
+++ b/packages/shadcn/src/commands/eject.ts
@@ -0,0 +1,248 @@
+import { promises as fs } from "fs"
+import path from "path"
+import { SHADCN_URL } from "@/src/registry/constants"
+import { getConfig } from "@/src/utils/get-config"
+import {
+ formatMonorepoMessage,
+ getMonorepoTargets,
+ isMonorepoRoot,
+} from "@/src/utils/get-monorepo-info"
+import { getPackageInfo } from "@/src/utils/get-package-info"
+import { getPackageManager } from "@/src/utils/get-package-manager"
+import { handleError } from "@/src/utils/handle-error"
+import { highlighter } from "@/src/utils/highlighter"
+import { logger } from "@/src/utils/logger"
+import { spinner } from "@/src/utils/spinner"
+import { Command } from "commander"
+import { execa } from "execa"
+import fsExtra from "fs-extra"
+import prompts from "prompts"
+import { z } from "zod"
+
+export const SHADCN_TAILWIND_IMPORT =
+ /@import\s+["']shadcn\/tailwind\.css["'];?\s*\n?/
+
+export const ejectOptionsSchema = z.object({
+ cwd: z.string(),
+ yes: z.boolean(),
+ silent: z.boolean(),
+})
+
+export const eject = new Command()
+ .name("eject")
+ .description("inline shadcn/tailwind.css and remove the shadcn dependency")
+ .option(
+ "-c, --cwd ",
+ "the working directory. defaults to the current directory.",
+ process.cwd()
+ )
+ .option("-y, --yes", "skip confirmation prompt.", false)
+ .option("-s, --silent", "mute output.", false)
+ .action(async (opts) => {
+ try {
+ const options = ejectOptionsSchema.parse({
+ cwd: path.resolve(opts.cwd),
+ yes: opts.yes,
+ silent: opts.silent,
+ })
+
+ await runEject(options)
+ } catch (error) {
+ handleError(error)
+ }
+ })
+
+export async function runEject(options: z.infer) {
+ if (!fsExtra.existsSync(path.resolve(options.cwd, "components.json"))) {
+ if (await isMonorepoRoot(options.cwd)) {
+ const targets = await getMonorepoTargets(options.cwd)
+ if (targets.length > 0) {
+ formatMonorepoMessage("eject", targets)
+ process.exit(1)
+ }
+ }
+
+ logger.break()
+ logger.error(
+ `No ${highlighter.info("components.json")} found. Run ${highlighter.info("shadcn init")} first.`
+ )
+ logger.error(
+ `Learn more at ${highlighter.info(`${SHADCN_URL}/docs/components-json`)}.`
+ )
+ logger.break()
+ process.exit(1)
+ }
+
+ const config = await getConfig(options.cwd)
+ if (!config?.resolvedPaths.tailwindCss) {
+ logger.break()
+ logger.error(
+ "Could not resolve the Tailwind CSS file from components.json."
+ )
+ logger.break()
+ process.exit(1)
+ }
+
+ const cssFilepath = config.resolvedPaths.tailwindCss
+ const cssFilepathRelative = path.relative(options.cwd, cssFilepath)
+ let cssContent = await fs.readFile(cssFilepath, "utf8")
+
+ if (!SHADCN_TAILWIND_IMPORT.test(cssContent)) {
+ logger.break()
+ logger.error(
+ `Could not find ${highlighter.info('@import "shadcn/tailwind.css"')} in ${highlighter.info(cssFilepathRelative)}.`
+ )
+ logger.error("Nothing to eject.")
+ logger.break()
+ process.exit(1)
+ }
+
+ const packageInfo = getPackageInfo(options.cwd, false)
+ const shadcnVersion = getShadcnVersion(packageInfo)
+ const shadcnCssPath = resolveShadcnTailwindCss(options.cwd)
+ const shadcnCssContent = await fs.readFile(shadcnCssPath, "utf8")
+
+ if (!options.silent) {
+ logger.break()
+ logger.warn(
+ "This action is not reversible. Future shadcn CLI updates to tailwind.css will not apply automatically."
+ )
+ logger.break()
+ }
+
+ if (!options.yes) {
+ logger.log("This will:")
+ logger.log(
+ ` - Inline ${highlighter.info("shadcn/tailwind.css")} into ${highlighter.info(cssFilepathRelative)}`
+ )
+ logger.log(` - Remove the ${highlighter.info("shadcn")} dependency`)
+ logger.break()
+
+ const { proceed } = await prompts({
+ type: "confirm",
+ name: "proceed",
+ message: "Proceed?",
+ initial: false,
+ })
+
+ if (!proceed) {
+ process.exit(0)
+ }
+ }
+
+ const ejectSpinner = spinner(
+ `Inlining ${highlighter.info("shadcn/tailwind.css")}.`,
+ {
+ silent: options.silent,
+ }
+ )?.start()
+
+ cssContent = cssContent.replace(
+ SHADCN_TAILWIND_IMPORT,
+ () =>
+ `/* ejected from shadcn@${shadcnVersion} */\n${shadcnCssContent.trim()}\n\n`
+ )
+
+ await fs.writeFile(cssFilepath, cssContent, "utf8")
+ ejectSpinner?.succeed()
+
+ if (hasShadcnDependency(packageInfo)) {
+ const removeSpinner = spinner(`Removing ${highlighter.info("shadcn")}.`, {
+ silent: options.silent,
+ })?.start()
+
+ await removeShadcnDependency(options.cwd)
+ removeSpinner?.succeed()
+ } else if (!options.silent) {
+ logger.warn(
+ `The ${highlighter.info("shadcn")} package was not found in package.json. Skipped removal.`
+ )
+ }
+
+ logger.break()
+ logger.log(
+ `Ejected ${highlighter.info("shadcn/tailwind.css")} into ${highlighter.info(cssFilepathRelative)}.`
+ )
+ logger.break()
+}
+
+function getShadcnVersion(packageInfo: ReturnType) {
+ if (!packageInfo) {
+ return "unknown"
+ }
+
+ return (
+ packageInfo.dependencies?.shadcn ??
+ packageInfo.devDependencies?.shadcn ??
+ "unknown"
+ )
+ .replace(/^[\^~]/, "")
+ .trim()
+}
+
+function hasShadcnDependency(packageInfo: ReturnType) {
+ if (!packageInfo) {
+ return false
+ }
+
+ return Boolean(
+ packageInfo.dependencies?.shadcn || packageInfo.devDependencies?.shadcn
+ )
+}
+
+function resolveShadcnTailwindCss(cwd: string) {
+ const projectCss = path.join(cwd, "node_modules/shadcn/dist/tailwind.css")
+ if (fsExtra.existsSync(projectCss)) {
+ return projectCss
+ }
+
+ const cliRoot = process.argv[1]
+ ? path.dirname(path.resolve(process.argv[1]))
+ : cwd
+
+ for (const candidate of [
+ path.join(cliRoot, "tailwind.css"),
+ path.join(cliRoot, "dist", "tailwind.css"),
+ path.join(cliRoot, "src", "tailwind.css"),
+ path.join(process.cwd(), "src/tailwind.css"),
+ path.join(process.cwd(), "dist/tailwind.css"),
+ ]) {
+ if (fsExtra.existsSync(candidate)) {
+ return candidate
+ }
+ }
+
+ throw new Error("Could not resolve shadcn/tailwind.css.")
+}
+
+async function removeShadcnDependency(cwd: string) {
+ const packageManager = await getPackageManager(cwd)
+
+ switch (packageManager) {
+ case "npm":
+ await execa("npm", ["uninstall", "shadcn"], { cwd })
+ break
+ case "pnpm":
+ await execa("pnpm", ["remove", "shadcn"], { cwd })
+ break
+ case "yarn":
+ await execa("yarn", ["remove", "shadcn"], { cwd })
+ break
+ case "bun":
+ await execa("bun", ["remove", "shadcn"], { cwd })
+ break
+ case "deno": {
+ const packageJsonPath = path.join(cwd, "package.json")
+ const packageJson = await fsExtra.readJson(packageJsonPath)
+
+ for (const field of ["dependencies", "devDependencies"] as const) {
+ if (packageJson[field]?.shadcn) {
+ delete packageJson[field].shadcn
+ }
+ }
+
+ await fsExtra.writeJson(packageJsonPath, packageJson, { spaces: 2 })
+ break
+ }
+ }
+}
diff --git a/packages/shadcn/src/index.ts b/packages/shadcn/src/index.ts
index c0e9b6ca4a..39a3d68be4 100644
--- a/packages/shadcn/src/index.ts
+++ b/packages/shadcn/src/index.ts
@@ -4,6 +4,7 @@ import { apply } from "@/src/commands/apply"
import { build } from "@/src/commands/build"
import { diff } from "@/src/commands/diff"
import { docs } from "@/src/commands/docs"
+import { eject } from "@/src/commands/eject"
import { info } from "@/src/commands/info"
import { init } from "@/src/commands/init"
import { mcp } from "@/src/commands/mcp"
@@ -38,6 +39,7 @@ async function main() {
.addCommand(view)
.addCommand(search)
.addCommand(migrate)
+ .addCommand(eject)
.addCommand(info)
.addCommand(build)
.addCommand(mcp)