Traps already paid for
Each of these presents as something other than what it is. Read this before you start debugging something that "should work".
A new --text-* tier silently deletes itself
tailwind-merge resolves conflicts from a static map of Tailwind's class groups.
A text-* name it does not recognise as a font size is filed as a
colour, and the next text-* class in the same cn() call deletes it.
Register every tier in TYPE_TIERS in lib/utils.ts on the same commit that
adds it to @theme. This is not hypothetical: cn(typography.label, "text-muted-foreground") dropped text-label and rendered every eyebrow at the
inherited size.
--shadow-card: var(--shadow-card) inside @theme resolves to nothing
It is a self-reference. Radius and shadow are reached as
rounded-[var(--radius-card)] and shadow-[var(--shadow-card)]; the minted
shadow-overlay / shadow-modal utilities point at --elevation-* for exactly
this reason.
Re-running the shadcn CLI reverts the patches
The 31 primitives were generated and then edited — Button's four variants and its two radii, Input and Select's surface fills, the table's sticky header, pinned columns and row ladder, the sidebar's collapsed rail. Re-generating gives you stock shadcn back, and the diff looks like an upgrade. Copy from this kit instead.
SidebarMenuButton with a tooltip cannot be a DropdownMenuTrigger asChild
The tooltip prop wraps the button in <Tooltip><TooltipTrigger asChild>
unconditionally — hidden only suppresses the CONTENT while the rail is
expanded. The outer trigger then clones <Tooltip>, a component rather than a
DOM node, and its handlers, ref and data-state go nowhere.
Symptom: a menu that opens and closes at random and never reproduces on demand. It reads as a browser extension or a flaky harness. The switcher and the account menu therefore pass no tooltip; the cost is no label on those two buttons when the rail is collapsed.
A background tab freezes CSS transitions at their start value
getComputedStyle then reports the pre-transition number — measuring the
collapsed rail returned 224px for a 40px element, and even an inline
width: … !important appeared not to apply. Bring the tab to the front, or
inject * { transition: none !important } before measuring.
Anything painting outside CSS cannot read custom properties
A WebGL map, a canvas, a chart library, a generated PDF. Resolve the tokens with
getComputedStyle and re-read them on a MutationObserver when data-theme
flips, or the surface keeps the old theme's colours.
A full-viewport route needs a definite height
min-h-svh is a minimum, and overflow-hidden on a descendant of an
auto-height ancestor clamps nothing. Give the wrapper and the inset a real
height. The inset subtracts 1rem because the inset sidebar variant carries
m-2 — 8px top and bottom. globals.css does this by attribute
([data-map-route]), so the shell does not have to know which route is
rendering.
data-theme and hydration
next-themes writes the attribute before React hydrates, so <html> needs
suppressHydrationWarning. Never read the theme in JS to pick an asset — the
light/dark logo pair swaps in CSS for exactly this reason.
animate-in / fade-in-0 do nothing without tw-animate-css
The overlays carry those classes; the utilities only exist if globals.css
imports the plugin. The source app does not, so its dialogs open without
animation. Add @import "tw-animate-css"; if you want them.
Building while a dev server runs corrupts it
They share .next. Symptom: a runtime error inside a component you already
deleted, with a stack at line numbers that no longer exist. Stop dev before
building; recover with rm -rf .next.