The vendored folder
Read when: running add, apply or migrate, updating a component from upstream, changing
how a registry component looks or behaves, importing from reka-ui, or picking icons.
Owns: what the CLI overwrites, the update workflow, customising through wrappers (with code),
the reka-ui import rule, the icon package, community registry items.
TL;DR
components/ui/is CLI output. Never hand-edit it; customise one layer up, incomponents/.addcan rewrite the stylesheet. Readgit diffafter every CLI run and put back what it dropped.- Re-adding an installed component,
--overwriteandapplyreplace your files. Ask first. - Import the shadcn-vue component, not
reka-ui, except forConfigProvider, types, and a primitive the registry does not wrap. - One icon package, the one in
components.json.
Two folders, one rule each
| Folder | Holds | Rule |
|---|---|---|
components/ui/ |
registry components, written by the CLI | never hand-edit |
components/ |
your wrappers and compositions | every project decision lives here |
Why: an edit in components/ui/ has no record of what is yours and what is upstream, and one "yes"
to the overwrite question (or one --overwrite) reverts it. If a component seems to need a project-wide change,
it is almost always a token (theme.md) or a wrapper.
What the CLI writes
| Command | Writes |
|---|---|
add <name> |
the component folder, the registry components it depends on (adding sidebar also brings sheet, tooltip and others), npm packages, and CSS variables or font imports into the stylesheet |
add <installed name> |
the same files again. For each file that differs it asks "already exists. Would you like to overwrite?" (default no); --overwrite skips the question. A dependency you edited gets the same question, so read each one before answering |
add --overwrite, add --all -o |
replaces every existing file it touches |
apply <preset> |
overwrites config, fonts, CSS variables and every detected component |
init --preset <p> --force --no-reinstall |
config and CSS only; leaves components as they are |
migrate rtl [path] |
rewrites physical classes in components/ui/ and sets "rtl": true |
migrate icons |
swaps the icon library in your components |
Seen on the stylesheet with 2.8.x (checked 2026-11-03): add re-syncs the Google Fonts @import
lines to its own font set, dropping one it does not know (an Arabic face), and can append a second
@layer base. After every CLI run:
git diff -- components.json resources/css/app.css # your stylesheet path
git diff --stat -- resources/js/components/ui # only what you meant to add
Updating a component from upstream
In 2.8.2 add --dry-run, --diff and --view exist in --help but print "not yet supported", and
the diff command is not on the docs page (checked 2026-11-03). Use git as the diff tool:
- Commit or stash, so the tree is clean.
npx shadcn-vue@latest add <name> --overwrite(with the user's yes).git diff -- resources/js/components/ui/<name>: upstream changes against your copy.- Keep the upstream hunks, revert anything that undoes a deliberate change, re-run the type check.
Switching a preset: ask whether to overwrite (apply), merge (init --preset … --force --no-reinstall, then update each component as above) or skip (the same init, nothing else).
Customising without editing: three patterns
1. A class constant, for one recurring treatment the variants lack. It stays a class, so the
vendored button keeps its source and survives the next add.
// lib/button.ts
export const solidDestructive =
'bg-destructive text-destructive-foreground hover:bg-destructive/90 dark:bg-destructive dark:hover:bg-destructive/90';
<Button variant="destructive" :class="solidDestructive">Delete</Button>
The dark: halves are not redundant: if the variant sets dark:bg-destructive/20, tailwind-merge
treats a prefixed class as a different key, so an unprefixed bg-destructive never replaces it.
2. A wrapper that maps meaning to look, so no screen decides it again. Variants through cva,
colours through tokens:
<!-- components/StatusBadge.vue -->
<script setup lang="ts">
import { cva, type VariantProps } from 'class-variance-authority';
import { Badge } from '@/components/ui/badge';
const statusBadge = cva('border-transparent', {
variants: {
tone: {
green: 'bg-tone-green/12 text-tone-green',
blue: 'bg-tone-blue/12 text-tone-blue',
orange: 'bg-tone-orange/12 text-tone-orange',
error: 'bg-destructive/12 text-destructive',
gray: 'bg-muted text-muted-foreground',
},
},
defaultVariants: { tone: 'gray' },
});
export type StatusTone = NonNullable<VariantProps<typeof statusBadge>['tone']>;
defineProps<{ tone?: StatusTone; label: string }>();
</script>
<template>
<Badge variant="outline" :class="statusBadge({ tone })">{{ label }}</Badge>
</template>
Let the server send the tone (or a status the client maps once), never a colour.
3. A composition wrapper, for markup every form repeats:
<!-- components/FormField.vue -->
<script setup lang="ts">
import type { HTMLAttributes } from 'vue';
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';
import { cn } from '@/lib/utils';
const props = defineProps<{
label: string;
for?: string;
description?: string;
error?: string;
class?: HTMLAttributes['class'];
}>();
</script>
<template>
<Field :data-invalid="!!error || undefined" :class="cn('content-start', props.class)">
<FieldLabel :for="props.for">{{ label }}</FieldLabel>
<slot />
<FieldDescription v-if="description">{{ description }}</FieldDescription>
<FieldError v-if="error" :errors="[error]" />
</Field>
</template>
Wrappers take class and merge it with cn() last, so a caller can still place them.
A hardcoded English string inside a registry component is overridden at the call site (a slot, a
prop, aria-label), never by editing the file.
The reka-ui import rule
shadcn-vue is built on Reka UI; its generated files import reka-ui, which is why the package stays
in dependencies. In your own code:
- Import the shadcn-vue component (
@/components/ui/dialog), not the Reka primitive. Why: the styling, slots and data attributes live in the wrapper, and a raw primitive looks unstyled. - Allowed:
ConfigProviderin the root layouts (direction and locale, see rtl.md), type-only imports (import type { DateValue } from 'reka-ui'), and a primitive the registry does not wrap, used inside a wrapper incomponents/, never on a page. - Rebuilding focus or keyboard handling that Reka already has is never the answer.
Icons
- One package: the one
iconLibrarynames.lucide→@lucide/vue,tabler→@tabler/icons-vue,phosphor→@phosphor-icons/vue,remixicon→@remixicon/vue,hugeicons→@hugeicons/vue.lucide-vue-nextis the legacy Lucide package; new code imports@lucide/vue(checked 2026-11-03). - Named imports only (
import { PencilIcon } from '@lucide/vue'), so the bundle keeps only those. - Colour an icon with a token (
text-muted-foreground); never a hex. - Pass an icon to a component as the component itself (
:icon="PencilIcon"), not a string key. - Inside a
Buttonor a registry component, no size class; elsewhere size withsize-*. - The same concept takes the same icon on every screen.
Community registries
- Name the registry explicitly (
@namespace/item); never guess one for the user. - After adding from one, read every added file: fix imports that assume
@/components/uiwhen your alias differs, swap icons from another package, and check the composition rules inSKILL.md.