All documents

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.