Tailwind CSS + shadcn/ui — Pitfalls¶
OWNER: alexander ALSO_USED_BY: floris, floor LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: tailwindcss ^4, shadcn ^3.8.4
Overview¶
Known failure modes when using Tailwind v4 and shadcn/ui in GE projects. Every pitfall here was either encountered in production or is a documented v4 migration trap. Agents MUST read this page before starting UI work.
Pitfall 1: Dynamic Class Names Are Purged¶
Tailwind scans source files as plain text — it does NOT execute JavaScript. Dynamically constructed class names are invisible to the scanner.
ANTI_PATTERN: String interpolation for Tailwind classes.
// BROKEN — Tailwind cannot detect these classes
const color = "blue"
className={`bg-${color}-500 text-${color}-100`}
// BROKEN — variable class names
const size = isLarge ? "lg" : "sm"
className={`text-${size}`}
FIX: Use complete class names that Tailwind can scan.
// CORRECT — complete strings, scannable
const colorClasses = {
blue: "bg-blue-500 text-blue-100",
red: "bg-red-500 text-red-100",
}
className={colorClasses[color]}
// CORRECT — ternary with full class names
className={isLarge ? "text-lg" : "text-sm"}
FIX: For truly dynamic values, use inline styles or CSS variables.
// CORRECT — dynamic values via CSS variable
style={{ "--progress": `${percent}%` } as React.CSSProperties}
className="w-[var(--progress)]"
Pitfall 2: Class Conflicts Without cn()¶
Tailwind classes can conflict. The last class in source order does NOT win — CSS specificity and stylesheet order determine the result.
ANTI_PATTERN: Overriding without cn().
// BROKEN — both bg classes exist, unpredictable winner
<Button className={`bg-primary ${isActive ? "bg-green-500" : ""}`} />
FIX: Always use cn() (which calls tailwind-merge) to resolve conflicts.
// CORRECT — tailwind-merge deduplicates intelligently
<Button className={cn("bg-primary", isActive && "bg-green-500")} />
CHECK: Anywhere className is built from multiple sources.
THEN: Use cn() — never string concatenation, never template literals.
Pitfall 3: Dark Mode Default Changed in v4¶
Tailwind v4 defaults to prefers-color-scheme: dark (media query).
The dark: variant uses the OS preference, NOT a .dark class on <html>.
ANTI_PATTERN: Assuming dark: works with a class toggle.
// BROKEN in v4 default — toggling class has no effect
document.documentElement.classList.toggle("dark")
FIX: For GE projects using data-theme attribute switching:
CHECK: The project supports user-controlled dark mode.
THEN: Verify @custom-variant dark is configured in the main CSS file.
IF: Only OS-preference dark mode is needed.
THEN: The v4 default is correct — no configuration needed.
Pitfall 4: Border Color Default Changed¶
In v4, the default border color is currentColor (was gray-200 in v3).
Adding border without specifying a color produces borders matching text color.
ANTI_PATTERN: Using border alone expecting a light gray border.
{/* v4: border will be currentColor (black on light bg) — not gray-200 */}
<div className="border rounded-lg">
FIX: Always specify border color explicitly.
Pitfall 5: @apply Ordering Issues¶
@apply in v4 works differently with cascade layers.
Classes applied via @apply may have different specificity than expected.
ANTI_PATTERN: Using @apply for complex component styles.
FIX: Prefer CVA + cn() for component variants. Reserve @apply for truly simple cases (base body styles, prose overrides).
CHECK: You are reaching for @apply.
IF: It is for a component with variants.
THEN: Use CVA instead.
IF: It is for a global base style (body, headings in prose).
THEN: @apply is acceptable.
Pitfall 6: Gradient Syntax Changed¶
v4 renames gradient utilities.
ANTI_PATTERN: Using v3 gradient syntax.
{/* v3 syntax — does not work in v4 */}
<div className="bg-gradient-to-r from-blue-500 to-purple-500">
FIX: Use v4 syntax.
Pitfall 7: Modifying components/ui/ Directly¶
shadcn components in components/ui/ are the upgrade-safe base.
Editing them means losing changes when re-installing or updating.
ANTI_PATTERN: Adding project-specific logic to components/ui/button.tsx.
FIX: Create a wrapper: components/app-button.tsx that imports from ui/button.
CHECK: A change is being made to a file in components/ui/.
IF: The change is project-specific behavior (loading states, analytics, custom variants).
THEN: Create a wrapper component instead.
IF: The change is fixing a shadcn bug or updating the base token mapping.
THEN: Editing components/ui/ is acceptable — document the change.
Pitfall 8: Zod v4 Error Handling¶
GE uses Zod for form validation with shadcn Form components.
ANTI_PATTERN: Accessing .errors on a ZodError.
FIX: Use .issues — Zod v4 removed .errors.
// BROKEN in Zod v4
try { schema.parse(data) } catch (e) { console.log(e.errors) }
// CORRECT
try { schema.parse(data) } catch (e) { console.log(e.issues) }
Pitfall 9: color-mix() Browser Support¶
Tailwind v4 uses color-mix() for opacity utilities (e.g., bg-primary/50).
This requires Safari 16.4+, Chrome 111+, Firefox 128+.
CHECK: The project targets older browsers. IF: Safari < 16.4 or Chrome < 111 is required. THEN: Do not use Tailwind v4 — stay on v3.4.
Pitfall 10: Server Component Styling¶
shadcn components using Radix primitives require "use client" because Radix uses React context and event handlers.
CHECK: A shadcn component is used in a Server Component file.
IF: The component uses Radix (Dialog, Dropdown, Popover, etc.).
THEN: Import from a Client Component wrapper or add "use client" to the file.
IF: The component is purely presentational (Card, Badge, Separator).
THEN: It can be used directly in Server Components.
Pitfall 11: Missing cn() Helper¶
The cn() function must exist at lib/utils.ts.
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
CHECK: The project is set up.
THEN: Verify lib/utils.ts exists with the cn() function.
IF: Missing.
THEN: Create it — every component depends on it.
Pitfall 12: Lucide Icon Sizing¶
Lucide icons have no default size. They render at 24x24 if no size is set. In dense UIs this is often too large.
ANTI_PATTERN: Using Lucide icons without size classes.
FIX: Always set explicit size.
Cross-References¶
READ_ALSO: wiki/docs/stack/tailwind-shadcn/index.md READ_ALSO: wiki/docs/stack/tailwind-shadcn/design-tokens.md READ_ALSO: wiki/docs/stack/tailwind-shadcn/component-patterns.md READ_ALSO: wiki/docs/stack/tailwind-shadcn/checklist.md READ_ALSO: wiki/docs/stack/nextjs/index.md