Skip to content

Pasting a shadcn component into a repo that renamed the tokens

shadcn components are written against variable names, not values. Publish those names alongside your own and a paste works unmodified: except for the ones you already claimed.

Glow4 min readships at docs/solutions/glow/pasting-shadcn-components-into-your-own-token-names.md

Tags: shadcn · tailwind · design-tokens · theming · react

You copy a dropdown menu out of the shadcn/ui docs into your project. It compiles, it opens, and it is invisible: white text on a white panel, with a menu item that turns into a grey slab on hover.

Nothing in the component is broken. It is asking for variables your project does not publish.

Why it happens

A shadcn component contains no colours. It contains names:

<div className="bg-popover text-popover-foreground border-border shadow-md">
  <div className="focus:bg-accent focus:text-accent-foreground">Profile</div>
</div>

Those utilities exist only if your Tailwind theme defines --color-popover, --color-popover-foreground, --color-border, --color-accent and --color-accent-foreground. In Tailwind v4 an unknown utility is not an error: it simply is not generated, so the element renders with no background at all and inherits whatever is behind it.

If your project named the same concepts differently (canvas, ink, hairline, surface-card) every one of those classes is a no-op.

The fix: publish both sets of names

You do not have to choose. A colour token is a variable, and a variable can have two names pointing at one value.

Declare the palette once, using shadcn's names as the raw layer:

:root {
  --background: #ffffff;
  --foreground: #09090b;
  --card: #ffffff;
  --card-foreground: #09090b;
  --popover: #ffffff;
  --popover-foreground: #09090b;
  --primary: #18181b;
  --primary-foreground: #fafafa;
  --destructive: #dc2626;
  --border: #e4e4e7;
  --input: #e4e4e7;
  --ring: #18181b;
}

Then map both naming systems onto it in one theme block:

@theme inline {
  /* your names */
  --color-canvas: var(--background);
  --color-ink: var(--foreground);
  --color-hairline: var(--border);

  /* shadcn's names, same values */
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-card: var(--card);
  --color-card-foreground: var(--card-foreground);
  --color-popover: var(--popover);
  --color-popover-foreground: var(--popover-foreground);
  --color-primary-foreground: var(--primary-foreground);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
}

Now bg-canvas and bg-background are the same rule with two names, and a pasted component works untouched. Use @theme inline rather than @theme: it emits background-color: var(--background) instead of copying today's hex, so the utilities follow a dark-mode swap.

The cost is one block of aliases, written once. The benefit is that the entire shadcn catalogue becomes copy-paste for your project, forever.

The names you cannot bridge

Sometimes a name is already taken and means something else in your system. Two are common:

muted. Upstream, --muted is a light grey surface and --muted-foreground is the secondary text colour. Plenty of design systems use muted for the text colour directly, so text-muted already exists and means #71717a. You cannot have bg-muted be a pale surface and text-muted be a mid grey: one variable, one value.

accent. Upstream, --accent is the hover surface for menu items. Many systems use accent for their one saturated brand colour. Again, one variable.

You have to pick, and the right answer is almost always keep your own meaning, because your codebase already has dozens of usages and the paste has one or two. Then write the translation down where a person doing the paste will see it:

Upstream classHerePaste this instead
bg-mutedmuted is the muted text colourbg-surface-card
bg-accent / text-accent-foregroundaccent is the saturated brand colourbg-surface-card / text-ink

Two classes, and they cluster: they appear in menu items, command palettes and list rows. A grep on the way in catches them:

grep -nE "\b(bg|text|border)-(muted|accent)\b" src/components/ui/dropdown-menu.tsx

Note the word boundary. text-muted-foreground and bg-accent-soft are different tokens and are fine; only the bare names collide.

Other things a paste brings with it

cva and tailwind-merge. Upstream examples often use class-variance-authority and cn(). If your repo deliberately carries neither, rewrite the variant object as a plain lookup:

const variants: Record<Variant, string> = {
  default: "bg-primary text-primary-foreground",
  outline: "border border-input bg-background",
};

It is the same information with one fewer dependency, and it stays readable up to about five variants.

Radix packages. The interactive components genuinely depend on @radix-ui/react-*. Add the package properly rather than deleting the import and hand-rolling a focus trap: accessible menus are harder than they look.

Hard-coded values in examples. Docs examples sometimes carry an arbitrary value or a palette class from the demo page. A lint that fails on raw colours catches these at the door, which is a good reason to have one.

How to know it worked

Render the pasted component and check three things: it has a background, its border is visible, and its hover state is a surface rather than a saturated block. Then check it again in dark mode if you have one: a bridged token follows the mode, a missed one does not, which makes dark mode an unusually good detector for classes you forgot to translate.