Merge pull request #10451 from shadcn-ui/shadcn/apply-only

feat: add apply --only
This commit is contained in:
shadcn
2026-04-21 16:20:14 +04:00
committed by GitHub
10 changed files with 606 additions and 22 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": minor
---
add apply --only

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest"
import { buildRegistryBase, DEFAULT_CONFIG } from "@/registry/config"
import { GET } from "./route"
function createRequest(search = "") {
const searchParams = new URLSearchParams(
Object.entries(DEFAULT_CONFIG).map(([key, value]) => [key, String(value)])
)
const url = new URL(`http://localhost:4000/init${search}`)
for (const [key, value] of url.searchParams) {
searchParams.set(key, value)
}
return {
nextUrl: new URL(`http://localhost:4000/init?${searchParams}`),
} as Parameters<typeof GET>[0]
}
describe("GET /init", () => {
it("returns the full registry base when only is omitted", async () => {
const response = await GET(createRequest())
const json = await response.json()
expect(response.status).toBe(200)
expect(json).toEqual(buildRegistryBase(DEFAULT_CONFIG))
})
it("returns a sparse registry base when only is provided", async () => {
const response = await GET(createRequest("?only=theme"))
const json = await response.json()
expect(response.status).toBe(200)
expect(json.type).toBe("registry:base")
expect(json.config).toEqual({
menuColor: "default",
menuAccent: "subtle",
tailwind: {
baseColor: "neutral",
},
})
expect(json.cssVars.light).toBeDefined()
expect(json.cssVars.light.radius).toBe("0.625rem")
expect(json.dependencies).toBeUndefined()
expect(json.registryDependencies).toBeUndefined()
})
it("rejects unsupported only values", async () => {
const response = await GET(createRequest("?only=icon"))
const json = await response.json()
expect(response.status).toBe(400)
expect(json.error).toBe(
"Invalid only value. Use one or more of: theme, font"
)
})
})

View File

@@ -3,7 +3,11 @@ import { track } from "@vercel/analytics/server"
import { isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import { buildRegistryBase } from "@/registry/config"
import {
buildPartialRegistryBase,
buildRegistryBase,
parseRegistryBaseParts,
} from "@/registry/config"
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
@@ -16,13 +20,20 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
const onlyResult = parseRegistryBaseParts(searchParams.get("only"))
if (!onlyResult.success) {
return NextResponse.json({ error: onlyResult.error }, { status: 400 })
}
const rawPreset = searchParams.get("preset")
const presetCode =
rawPreset && isPresetCode(rawPreset)
? rawPreset
: getPresetCode(result.data)
const registryBase = buildRegistryBase(result.data)
const registryBase = onlyResult.parts
? buildPartialRegistryBase(result.data, onlyResult.parts)
: buildRegistryBase(result.data)
const parseResult = registryItemSchema.safeParse(registryBase)
if (!parseResult.success) {

View File

@@ -93,6 +93,14 @@ Use the `apply` command to apply a preset to an existing project.
npx shadcn@latest apply --preset a2r6bw
```
You can apply only the theme or fonts from a preset without reinstalling UI components:
```bash
npx shadcn@latest apply --preset a2r6bw --only theme
```
Supported values for `--only` are `theme` and `font`.
**Options**
```bash
@@ -105,6 +113,7 @@ Arguments:
Options:
--preset <preset> preset configuration to apply
--only [parts] apply only parts of a preset: theme, font
-y, --yes skip confirmation prompt. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-s, --silent mute output. (default: false)

View File

@@ -1,9 +1,11 @@
import { describe, expect, it } from "vitest"
import {
buildPartialRegistryBase,
buildRegistryBase,
DEFAULT_CONFIG,
designSystemConfigSchema,
parseRegistryBaseParts,
} from "./config"
describe("buildRegistryBase", () => {
@@ -100,3 +102,105 @@ describe("buildRegistryBase", () => {
expect(result.success).toBe(false)
})
})
describe("parseRegistryBaseParts", () => {
it("returns undefined parts when only is omitted", () => {
expect(parseRegistryBaseParts(null)).toEqual({
success: true,
parts: undefined,
})
})
it("normalizes and dedupes only values", () => {
expect(parseRegistryBaseParts("theme,fonts,font")).toEqual({
success: true,
parts: ["theme", "font"],
})
})
it("rejects invalid only values", () => {
const result = parseRegistryBaseParts("theme,colors")
expect(result.success).toBe(false)
})
})
describe("buildPartialRegistryBase", () => {
it("builds a sparse theme payload", () => {
const result = buildPartialRegistryBase(
{
...DEFAULT_CONFIG,
baseColor: "taupe",
theme: "taupe",
chartColor: "taupe",
menuAccent: "bold",
menuColor: "inverted",
radius: "large",
},
["theme"]
)
expect(result.type).toBe("registry:base")
expect(result.extends).toBe("none")
expect(result.config).toEqual({
menuColor: "inverted",
menuAccent: "bold",
tailwind: {
baseColor: "taupe",
},
})
expect(result.cssVars?.light?.radius).toBe("0.875rem")
expect(result.cssVars?.light).toBeDefined()
expect(result.cssVars?.dark).toBeDefined()
expect(result.registryDependencies).toBeUndefined()
expect("dependencies" in result).toBe(false)
})
it("builds a sparse font payload", () => {
const result = buildPartialRegistryBase(
{
...DEFAULT_CONFIG,
font: "noto-sans",
fontHeading: "playfair-display",
},
["font"]
)
expect(result.registryDependencies).toEqual([
"font-noto-sans",
"font-heading-playfair-display",
])
expect(result.cssVars).toBeUndefined()
expect(result.config).toBeUndefined()
})
it("adds a heading fallback when font heading inherits", () => {
const result = buildPartialRegistryBase(
{
...DEFAULT_CONFIG,
font: "jetbrains-mono",
fontHeading: "inherit",
},
["font"]
)
expect(result.registryDependencies).toEqual(["font-jetbrains-mono"])
expect(result.cssVars?.theme?.["--font-heading"]).toBe("var(--font-mono)")
})
it("merges combined partial payloads", () => {
const result = buildPartialRegistryBase(
{
...DEFAULT_CONFIG,
font: "figtree",
},
["theme", "font"]
)
expect(result.config?.tailwind?.baseColor).toBe("neutral")
expect(result.registryDependencies).toEqual(["font-figtree"])
expect(result.cssVars?.light?.radius).toBe("0.625rem")
expect(result.cssVars?.light).toBeDefined()
expect(result.cssVars?.theme?.["--font-heading"]).toBe("var(--font-sans)")
})
})

View File

@@ -3,6 +3,7 @@ import {
type IconLibrary,
type IconLibraryName,
} from "shadcn/icons"
import { type RegistryItem } from "shadcn/schema"
import { z } from "zod"
import { BASE_COLORS, type BaseColor } from "@/registry/base-colors"
@@ -25,6 +26,8 @@ export type StyleName = Style["name"]
export type ThemeName = Theme["name"]
export type BaseColorName = BaseColor["name"]
export type ChartColorName = Theme["name"]
export const REGISTRY_BASE_PARTS = ["theme", "font"] as const
export type RegistryBasePart = (typeof REGISTRY_BASE_PARTS)[number]
// Derive font values from registry fonts (e.g., "font-inter" -> "inter").
const fontValues = bodyFonts.map((f) => f.name.replace("font-", "")) as [
@@ -476,6 +479,35 @@ export function getIconLibrary(name: IconLibraryName) {
return iconLibraries[name]
}
export function parseRegistryBaseParts(value: string | null) {
if (value === null) {
return { success: true as const, parts: undefined }
}
const aliases: Record<string, RegistryBasePart> = {
theme: "theme",
font: "font",
fonts: "font",
}
const rawParts = value
.split(",")
.map((part) => part.trim().toLowerCase())
.filter(Boolean)
const invalid = rawParts.filter((part) => !aliases[part])
if (!rawParts.length || invalid.length) {
return {
success: false as const,
error: `Invalid only value. Use one or more of: ${REGISTRY_BASE_PARTS.join(", ")}`,
}
}
return {
success: true as const,
parts: Array.from(new Set(rawParts.map((part) => aliases[part]))),
}
}
// Builds a registry:theme item from a design system config.
export function buildRegistryTheme(config: DesignSystemConfig) {
const baseColor = getBaseColor(config.baseColor)
@@ -614,3 +646,68 @@ export function buildRegistryBase(config: DesignSystemConfig) {
}),
}
}
export function buildPartialRegistryBase(
config: DesignSystemConfig,
parts: RegistryBasePart[]
) {
const uniqueParts = Array.from(new Set(parts))
const normalizedFontHeading =
config.fontHeading === config.font ? "inherit" : config.fontHeading
const partialConfig: {
menuColor?: DesignSystemConfig["menuColor"]
menuAccent?: DesignSystemConfig["menuAccent"]
tailwind?: {
baseColor?: string
}
} = {}
const registryDependencies: string[] = []
const cssVars: NonNullable<RegistryItem["cssVars"]> = {}
if (uniqueParts.includes("theme")) {
const registryTheme = buildRegistryTheme(config)
partialConfig.menuColor = config.menuColor
partialConfig.menuAccent = config.menuAccent
partialConfig.tailwind = {
baseColor: config.baseColor,
}
if (registryTheme.cssVars.theme) {
cssVars.theme = {
...(cssVars.theme ?? {}),
...registryTheme.cssVars.theme,
}
}
cssVars.light = {
...(cssVars.light ?? {}),
...registryTheme.cssVars.light,
}
cssVars.dark = {
...(cssVars.dark ?? {}),
...registryTheme.cssVars.dark,
}
}
if (uniqueParts.includes("font")) {
registryDependencies.push(`font-${config.font}`)
if (normalizedFontHeading !== "inherit") {
registryDependencies.push(`font-heading-${normalizedFontHeading}`)
} else {
cssVars.theme = {
...(cssVars.theme ?? {}),
"--font-heading": getInheritedHeadingFontValue(config.font),
}
}
}
return {
name: `${config.base}-${config.style}-${uniqueParts.join("-")}`,
extends: "none",
type: "registry:base" as const,
...(Object.keys(partialConfig).length > 0 && { config: partialConfig }),
...(registryDependencies.length > 0 && { registryDependencies }),
...(Object.keys(cssVars).length > 0 && { cssVars }),
} satisfies RegistryItem
}

View File

@@ -1,7 +1,13 @@
import { REGISTRY_URL } from "@/src/registry/constants"
import { describe, expect, it } from "vitest"
import { resolveApplyInitUrl } from "./apply"
import {
getPresetUrlOnly,
parseApplyOnlyParts,
resolveApplyInitUrl,
resolveApplyOnly,
validateApplyOnlyPreset,
} from "./apply"
const SHADCN_URL = REGISTRY_URL.replace(/\/r\/?$/, "")
@@ -46,4 +52,128 @@ describe("resolveApplyInitUrl", () => {
expect(parsed.searchParams.get("base")).toBe("base")
expect(parsed.searchParams.get("rtl")).toBe("true")
})
it("should include only for preset codes", () => {
const initUrl = resolveApplyInitUrl("a0", "base", {
template: "next",
rtl: true,
only: "theme,font",
})
const parsed = new URL(initUrl)
expect(parsed.searchParams.get("only")).toBe("theme,font")
})
it("should include only for named presets", () => {
const initUrl = resolveApplyInitUrl("lyra", "base", {
template: "next",
rtl: true,
only: "font",
})
const parsed = new URL(initUrl)
expect(parsed.searchParams.get("only")).toBe("font")
})
it("should include only for raw preset URLs", () => {
const presetUrl = `${SHADCN_URL}/init?base=radix&style=nova&baseColor=neutral&theme=neutral&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default`
const initUrl = resolveApplyInitUrl(presetUrl, "base", {
template: "next",
rtl: true,
only: "theme",
})
const parsed = new URL(initUrl)
expect(parsed.searchParams.get("only")).toBe("theme")
})
it("should preserve only from raw preset URLs", () => {
const presetUrl = `${SHADCN_URL}/init?base=radix&style=nova&baseColor=neutral&theme=neutral&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&only=font`
const initUrl = resolveApplyInitUrl(presetUrl, "base", {
template: "next",
rtl: true,
})
const parsed = new URL(initUrl)
expect(parsed.searchParams.get("only")).toBe("font")
})
})
describe("parseApplyOnlyParts", () => {
it("returns undefined when only is omitted", () => {
expect(resolveApplyOnly(undefined)).toBeUndefined()
})
it("rejects missing only values with allowed values", () => {
expect(() => resolveApplyOnly(true)).toThrow(
[
"Missing value for --only.",
"Use one or more of: theme, font.",
"Example: shadcn apply <preset> --only theme,font.",
].join("\n")
)
})
it("normalizes explicit values and aliases", () => {
expect(resolveApplyOnly("font")).toEqual(["font"])
expect(parseApplyOnlyParts("theme,font")).toEqual(["theme", "font"])
})
it("dedupes and accepts plural aliases", () => {
expect(parseApplyOnlyParts("theme,fonts,font")).toEqual(["theme", "font"])
})
it("rejects invalid values", () => {
expect(() => parseApplyOnlyParts("theme,colors")).toThrow(
[
"Invalid value for --only: theme,colors.",
"Use one or more of: theme, font.",
"Example: shadcn apply <preset> --only theme,font.",
].join("\n")
)
expect(() => parseApplyOnlyParts("")).toThrow("Invalid value for --only")
expect(() => parseApplyOnlyParts("icon")).toThrow(
"Use one or more of: theme, font."
)
})
})
describe("getPresetUrlOnly", () => {
it("reads only from shadcn init URLs", () => {
const presetUrl = `${SHADCN_URL}/init?base=radix&style=nova&only=font`
expect(getPresetUrlOnly(presetUrl)).toBe("font")
})
it("reads only from non-shadcn init URLs", () => {
const presetUrl =
"http://localhost:4000/init?base=radix&style=nova&only=font"
expect(getPresetUrlOnly(presetUrl)).toBe("font")
expect(resolveApplyOnly(getPresetUrlOnly(presetUrl))).toEqual(["font"])
})
it("ignores only on non-init URLs", () => {
const presetUrl =
"http://localhost:4000/r/styles/nova/button.json?only=font"
expect(getPresetUrlOnly(presetUrl)).toBeUndefined()
})
})
describe("validateApplyOnlyPreset", () => {
it("rejects only without a preset", () => {
expect(() => validateApplyOnlyPreset({ only: ["theme"] })).toThrow(
[
"Missing preset for --only.",
"Use: shadcn apply <preset> --only theme,font.",
].join("\n")
)
})
it("allows only with a preset", () => {
expect(() =>
validateApplyOnlyPreset({ preset: "a0", only: ["theme"] })
).not.toThrow()
})
})

View File

@@ -33,15 +33,27 @@ export const applyOptionsSchema = z.object({
cwd: z.string(),
positionalPreset: z.string().optional(),
preset: z.string().optional(),
only: z.union([z.boolean(), z.string()]).optional(),
yes: z.boolean(),
silent: z.boolean(),
})
const APPLY_ONLY_VALUES = ["theme", "font"] as const
type ApplyOnlyValue = (typeof APPLY_ONLY_VALUES)[number]
class ApplyOnlyError extends Error {
constructor(message: string) {
super(message)
this.name = "ApplyOnlyError"
}
}
export const apply = new Command()
.name("apply")
.description("apply a preset to an existing project")
.argument("[preset]", "the preset to apply")
.option("--preset <preset>", "preset configuration to apply")
.option("--only [parts]", "apply only parts of a preset: theme, font")
.option("-y, --yes", "skip confirmation prompt.", false)
.option(
"-c, --cwd <cwd>",
@@ -58,6 +70,8 @@ export const apply = new Command()
})
const preset = resolveApplyPreset(options)
const explicitOnly = resolveApplyOnly(options.only)
validateApplyOnlyPreset({ preset, only: explicitOnly })
const preflight = await preFlightApply(options)
@@ -114,26 +128,43 @@ export const apply = new Command()
validatePreset(preset)
const reinstallComponents = await getProjectComponents(options.cwd)
const only = explicitOnly ?? resolveApplyOnly(getPresetUrlOnly(preset))
const shouldReinstallComponents = !only
const reinstallComponents = shouldReinstallComponents
? await getProjectComponents(options.cwd)
: []
if (!options.yes) {
logger.break()
logger.warn(
highlighter.warn(
`Applying a new preset will overwrite existing UI components, fonts, and CSS variables.`
if (!only) {
logger.warn(
highlighter.warn(
`Applying a new preset will overwrite existing UI components, fonts, and CSS variables.`
)
)
)
} else {
logger.warn(
highlighter.warn(
`Applying the selected preset parts will update your project configuration and styles.`
)
)
}
logger.warn(
`Commit or stash your changes before continuing so you can easily go back.`
)
logger.break()
logger.log(" The following components will be re-installed:")
if (reinstallComponents.length) {
for (let i = 0; i < reinstallComponents.length; i += 8) {
logger.log(` - ${reinstallComponents.slice(i, i + 8).join(", ")}`)
if (shouldReinstallComponents) {
logger.break()
logger.log(" The following components will be re-installed:")
if (reinstallComponents.length) {
for (let i = 0; i < reinstallComponents.length; i += 8) {
logger.log(
` - ${reinstallComponents.slice(i, i + 8).join(", ")}`
)
}
} else {
logger.log(" - No installed UI components were detected.")
}
} else {
logger.log(" - No installed UI components were detected.")
}
logger.break()
@@ -156,6 +187,7 @@ export const apply = new Command()
const initUrl = resolveApplyInitUrl(preset, currentBase, {
template,
rtl,
only: only?.join(","),
})
await withFileBackup(
@@ -171,17 +203,23 @@ export const apply = new Command()
| undefined,
})
const applyRegistryBaseConfig = resolveApplyRegistryBaseConfig({
registryBaseConfig,
existingConfig,
only,
})
await runInit({
cwd: options.cwd,
yes: true,
force: false,
reinstall: true,
reinstall: shouldReinstallComponents,
defaults: false,
silent: options.silent,
isNewProject: false,
cssVariables: true,
installStyleIndex,
registryBaseConfig,
registryBaseConfig: applyRegistryBaseConfig,
existingConfig,
components: [cleanUrl, ...reinstallComponents],
})
@@ -201,6 +239,14 @@ export const apply = new Command()
logger.log("Preset applied successfully.")
logger.break()
} catch (error) {
if (error instanceof ApplyOnlyError) {
for (const line of error.message.split("\n")) {
logger.error(line)
}
logger.break()
process.exit(1)
}
logger.break()
handleError(error)
} finally {
@@ -225,6 +271,118 @@ function resolveApplyPreset(options: z.infer<typeof applyOptionsSchema>) {
return flagPreset ?? positionalPreset
}
export function getPresetUrlOnly(preset: string) {
if (!isUrl(preset)) {
return undefined
}
const url = new URL(preset)
if (url.pathname !== "/init") {
return undefined
}
return url.searchParams.get("only") ?? undefined
}
export function resolveApplyOnly(
value: z.infer<typeof applyOptionsSchema>["only"]
) {
if (value === undefined || value === false) {
return undefined
}
if (value === true) {
throw new ApplyOnlyError(
[
"Missing value for --only.",
`Use one or more of: ${APPLY_ONLY_VALUES.join(", ")}.`,
"Example: shadcn apply <preset> --only theme,font.",
].join("\n")
)
}
return parseApplyOnlyParts(value)
}
export function parseApplyOnlyParts(value: string) {
const aliases: Record<string, ApplyOnlyValue> = {
theme: "theme",
font: "font",
fonts: "font",
}
const parts = value
.split(",")
.map((part) => part.trim().toLowerCase())
.filter(Boolean)
const invalid = parts.filter((part) => !aliases[part])
if (!parts.length || invalid.length) {
throw new ApplyOnlyError(
[
`Invalid value for --only: ${value}.`,
`Use one or more of: ${APPLY_ONLY_VALUES.join(", ")}.`,
"Example: shadcn apply <preset> --only theme,font.",
].join("\n")
)
}
return Array.from(new Set(parts.map((part) => aliases[part])))
}
export function validateApplyOnlyPreset(options: {
preset?: string
only?: ApplyOnlyValue[]
}) {
if (!options.only || options.preset) {
return
}
throw new ApplyOnlyError(
[
"Missing preset for --only.",
"Use: shadcn apply <preset> --only theme,font.",
].join("\n")
)
}
function resolveApplyRegistryBaseConfig(options: {
registryBaseConfig: Record<string, unknown> | undefined
existingConfig: Record<string, unknown>
only: ApplyOnlyValue[] | undefined
}) {
if (!options.only || options.only.includes("theme")) {
return options.registryBaseConfig
}
const existingTailwind =
typeof options.existingConfig.tailwind === "object" &&
options.existingConfig.tailwind !== null
? options.existingConfig.tailwind
: {}
const registryTailwind =
typeof options.registryBaseConfig?.tailwind === "object" &&
options.registryBaseConfig.tailwind !== null
? options.registryBaseConfig.tailwind
: {}
const config: Record<string, unknown> = {
...options.registryBaseConfig,
tailwind: {
...existingTailwind,
...registryTailwind,
},
}
if (options.existingConfig.menuColor) {
config.menuColor = options.existingConfig.menuColor
}
if (options.existingConfig.menuAccent) {
config.menuAccent = options.existingConfig.menuAccent
}
return config
}
function validatePreset(preset: string) {
if (isUrl(preset) || isPresetCode(preset)) {
return
@@ -251,7 +409,7 @@ async function resolveApplyTemplate(cwd: string) {
export function resolveApplyInitUrl(
preset: string,
currentBase: "radix" | "base",
options: { template?: string; rtl?: boolean } = {}
options: { template?: string; rtl?: boolean; only?: string } = {}
) {
if (isUrl(preset)) {
const url = new URL(preset)
@@ -262,6 +420,9 @@ export function resolveApplyInitUrl(
url.searchParams.set("base", currentBase)
url.searchParams.set("rtl", String(options.rtl ?? false))
if (options.only) {
url.searchParams.set("only", options.only)
}
return url.toString()
}
@@ -280,7 +441,7 @@ export function resolveApplyInitUrl(
base: currentBase,
rtl: options.rtl ?? false,
},
{ preset, template: options.template }
{ preset, template: options.template, only: options.only }
)
}
@@ -292,7 +453,7 @@ export function resolveApplyInitUrl(
base: currentBase,
rtl: options.rtl ?? resolvedPreset.rtl,
},
{ template: options.template }
{ template: options.template, only: options.only }
)
}

View File

@@ -195,7 +195,7 @@ export function resolveInitUrl(
menuColor: string
radius: string
},
options?: { template?: string; preset?: string }
options?: { template?: string; preset?: string; only?: string }
) {
const params = new URLSearchParams({
base: preset.base,
@@ -228,6 +228,10 @@ export function resolveInitUrl(
params.set("template", options.template)
}
if (options?.only) {
params.set("only", options.only)
}
// Signal the server to record this init run.
params.set("track", "1")

View File

@@ -173,8 +173,9 @@ npx shadcn@latest docs button dialog select
6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project.
7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on.
8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user.
9. **Switching presets** — Ask the user first: **overwrite**, **merge**, or **skip**?
9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?
- **Overwrite**: `npx shadcn@latest apply --preset <code>`. Overwrites detected components, fonts, and CSS variables.
- **Partial**: `npx shadcn@latest apply --preset <code> --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms.
- **Merge**: `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually.
- **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.
- **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base <current-base>` explicitly — preset codes do not encode the base.
@@ -209,6 +210,9 @@ npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (ba
# Apply a preset to an existing project.
npx shadcn@latest apply --preset a2r6bw
npx shadcn@latest apply a2r6bw
npx shadcn@latest apply --preset a2r6bw --only theme
npx shadcn@latest apply --preset a2r6bw --only font
npx shadcn@latest apply --preset a2r6bw --only theme,font
# Add components.
npx shadcn@latest add button card dialog