Tools: I Know This Will Upset Some Devs, but Tailwind + Shadcn/ui + Shadow DOM = Pain

Tools: I Know This Will Upset Some Devs, but Tailwind + Shadcn/ui + Shadow DOM = Pain

Source: Dev.to

The dream vs. the reality ## The Setup ## Problem #1: Shadow DOM vs. Tailwind CSS - A Fundamental Conflict ## Why they don't get along ## The "solution" (with heavy air quotes) ## Problem #2: shadcn/ui and the Portal Problem ## What makes shadcn/ui special (and problematic) ## Example: The Accordion that works ## Example: The Dialog that breaks ## The workaround ## Problem #3: Dynamic Classes and CVA ## Class Variance Authority (CVA) complications ## The Shadow DOM problem ## The fix: Safelist everything ## Problem #4: Theme Variables and CSS Custom Properties ## How shadcn/ui does theming ## Shadow DOM breaks variable inheritance ## The solution: Replicate variables ## Real-World Impact: A Case Study ## With Shadow DOM enabled: ## Without Shadow DOM: ## So What's The Answer? ## When Shadow DOM makes sense: ## When to skip Shadow DOM: ## Hybrid approach (what actually works): ## The Uncomfortable Truth ## What we learned: ## What worked for us: ## Resources This recent post Is Learning CSS a Waste of Time in 2026? (by @sylwia-lask) really hit me, especially the part about accessibility dragging you straight back into raw CSS. Lately, with Tailwind and shadcn, most styling just… works. Move fast, tweak a class or two, done. Then Shadow DOM happened. Suddenly, stuff that “should just work” broke. Overrides stopped applying, styles got tricky, and all those abstractions felt thinner than expected. Not a Tailwind or shadcn complaint...just a reminder that knowing CSS still saves you when things fall apart. So here's the idea: build beautiful, reusable web components using React, style them with Tailwind CSS, use shadcn/ui for polished UI components, and wrap them up with Shadow DOM for perfect encapsulation. Sounds great, right? Well... not quite. Turns out these three technologies don't play nicely together. Here's what we learned the hard way. TL;DR: Shadow DOM + Tailwind + shadcn/ui = pain. Choose carefully based on your actual needs, not theoretical ideals. Sometimes the "impure" solution is the right one. Let's talk about what went wrong. Tailwind CSS is built on a simple idea: utility classes in a global stylesheet. You include one CSS file, and boom - every element on your page can use classes like bg-blue-500 or flex justify-center. Shadow DOM is built on the opposite idea: complete isolation. Styles inside Shadow DOM can't leak out, and styles from outside can't leak in. This is great for encapsulation, but terrible for Tailwind. Result: Your component renders, but it looks completely broken. No padding, no background color, no rounded corners. Nothing. All your Tailwind classes are ignored because the global Tailwind stylesheet can't penetrate the Shadow DOM boundary. You have to import the Tailwind CSS directly into each component: This works, but at a cost: Bundle size explosion Every web component bundles its own complete copy of Tailwind CSS. If you have 5 components on a page, you're loading Tailwind 5 times. That's 5x the CSS, all identical. No browser caching Since each component has its own bundled styles, you can't leverage browser caching for shared CSS. Every component download includes the same Tailwind utilities. Build complexity Your build tools need to handle CSS imports for each component separately, making your webpack/vite config more complex. shadcn/ui is built on Radix UI primitives, which are fantastic components. But they have one quirk that breaks with Shadow DOM: portals. Components like Dialog, Dropdown, Popover, Tooltip all use React portals to render their content outside the normal component tree, usually by appending to document.body. This is smart for z-index management and avoiding overflow issues, but it's a disaster for Shadow DOM. Why it works: Accordion renders everything in-place. No portals, no teleporting content. All the HTML stays within your component tree, so Shadow DOM can style it. You have to choose: Shadow DOM or portals. Can't have both. Option A: Disable Shadow DOM for portal-heavy components Now you need to manage styles globally and deal with potential class name conflicts. Option B: Only use non-portal components This limits your UI toolkit significantly. shadcn/ui uses CVA to handle component variants - different sizes, colors, and states. This generates Tailwind classes dynamically: All these dynamically generated classes need to exist in your Shadow DOM's stylesheet. But Tailwind's JIT (Just-In-Time) compiler only includes classes it finds in your files during build time. When CVA combines classes dynamically at runtime, Tailwind might not have included them in the build, leading to missing styles. The problem with safelist: shadcn/ui uses CSS custom properties (variables) for theming: Then in your Tailwind config: CSS custom properties inherit through the DOM tree, but Shadow DOM creates a boundary. Variables defined outside don't automatically flow in. Result: Your component can't access --background, --foreground, or --primary variables. All theme colors fallback to defaults or break entirely. You need to redeclare CSS variables inside your Shadow DOM: Problems with this approach: Let's look at what this means in practice. Say you're building a dashboard with these components: StatsCard: ✅ Works perfectly DataTable: ⚠️ Partially broken SettingsDialog: ❌ Completely broken Total bundle cost: 240KB of duplicated CSS for 3 components Shadow DOM, Tailwind CSS, and shadcn/ui are all great technologies on their own. But together? They fight each other. Shadow DOM wants: Complete isolation Tailwind wants: Global utility classes shadcn/ui wants: Portals for proper z-index management Pick two. You can't have all three working perfectly together. We ended up with a hybrid approach: Is it perfect? No. But it works, and that matters more than architectural purity. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: // Your React component with Tailwind classes export const MyCard = () => { return ( <div className="max-w-2xl w-full p-4 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold text-gray-900">Hello World</h1> <p className="text-gray-600 mt-2">This should look nice...</p> </div> ); }; // Wrap it as a web component with Shadow DOM const MyCardWC = r2wc(MyCard, { shadow: 'open' // Enable Shadow DOM }); customElements.define('my-card', MyCardWC); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Your React component with Tailwind classes export const MyCard = () => { return ( <div className="max-w-2xl w-full p-4 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold text-gray-900">Hello World</h1> <p className="text-gray-600 mt-2">This should look nice...</p> </div> ); }; // Wrap it as a web component with Shadow DOM const MyCardWC = r2wc(MyCard, { shadow: 'open' // Enable Shadow DOM }); customElements.define('my-card', MyCardWC); COMMAND_BLOCK: // Your React component with Tailwind classes export const MyCard = () => { return ( <div className="max-w-2xl w-full p-4 bg-white rounded-lg shadow-md"> <h1 className="text-2xl font-bold text-gray-900">Hello World</h1> <p className="text-gray-600 mt-2">This should look nice...</p> </div> ); }; // Wrap it as a web component with Shadow DOM const MyCardWC = r2wc(MyCard, { shadow: 'open' // Enable Shadow DOM }); customElements.define('my-card', MyCardWC); CODE_BLOCK: // styles.css @tailwind base; @tailwind components; @tailwind utilities; // Component file import './styles.css'; // Import for every component const MyCardWC = r2wc(MyCard, { shadow: 'open' }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // styles.css @tailwind base; @tailwind components; @tailwind utilities; // Component file import './styles.css'; // Import for every component const MyCardWC = r2wc(MyCard, { shadow: 'open' }); CODE_BLOCK: // styles.css @tailwind base; @tailwind components; @tailwind utilities; // Component file import './styles.css'; // Import for every component const MyCardWC = r2wc(MyCard, { shadow: 'open' }); COMMAND_BLOCK: import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@your-ui/components'; export const FAQ = () => { return ( <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>What is this?</AccordionTrigger> <AccordionContent> This is an accordion that actually works with Shadow DOM! </AccordionContent> </AccordionItem> </Accordion> ); }; const FAQWC = r2wc(FAQ, { shadow: 'open' }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@your-ui/components'; export const FAQ = () => { return ( <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>What is this?</AccordionTrigger> <AccordionContent> This is an accordion that actually works with Shadow DOM! </AccordionContent> </AccordionItem> </Accordion> ); }; const FAQWC = r2wc(FAQ, { shadow: 'open' }); COMMAND_BLOCK: import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@your-ui/components'; export const FAQ = () => { return ( <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>What is this?</AccordionTrigger> <AccordionContent> This is an accordion that actually works with Shadow DOM! </AccordionContent> </AccordionItem> </Accordion> ); }; const FAQWC = r2wc(FAQ, { shadow: 'open' }); COMMAND_BLOCK: import { Dialog, DialogTrigger, DialogContent } from '@your-ui/components'; export const MyDialog = () => { return ( <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent> <h2>This won't be styled properly!</h2> <p>The content is outside Shadow DOM now.</p> </DialogContent> </Dialog> ); }; const MyDialogWC = r2wc(MyDialog, { shadow: 'open' }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { Dialog, DialogTrigger, DialogContent } from '@your-ui/components'; export const MyDialog = () => { return ( <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent> <h2>This won't be styled properly!</h2> <p>The content is outside Shadow DOM now.</p> </DialogContent> </Dialog> ); }; const MyDialogWC = r2wc(MyDialog, { shadow: 'open' }); COMMAND_BLOCK: import { Dialog, DialogTrigger, DialogContent } from '@your-ui/components'; export const MyDialog = () => { return ( <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent> <h2>This won't be styled properly!</h2> <p>The content is outside Shadow DOM now.</p> </DialogContent> </Dialog> ); }; const MyDialogWC = r2wc(MyDialog, { shadow: 'open' }); CODE_BLOCK: // No Shadow DOM = portals work, but no encapsulation const MyDialogWC = r2wc(MyDialog, { shadow: null }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // No Shadow DOM = portals work, but no encapsulation const MyDialogWC = r2wc(MyDialog, { shadow: null }); CODE_BLOCK: // No Shadow DOM = portals work, but no encapsulation const MyDialogWC = r2wc(MyDialog, { shadow: null }); CODE_BLOCK: // ✅ Safe to use with Shadow DOM import { Accordion, Card, Badge, Button, Tabs, Progress } from '@your-ui/components'; // ❌ Don't use with Shadow DOM (they use portals) import { Dialog, Popover, Tooltip, DropdownMenu, Sheet, AlertDialog } from '@your-ui/components'; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // ✅ Safe to use with Shadow DOM import { Accordion, Card, Badge, Button, Tabs, Progress } from '@your-ui/components'; // ❌ Don't use with Shadow DOM (they use portals) import { Dialog, Popover, Tooltip, DropdownMenu, Sheet, AlertDialog } from '@your-ui/components'; CODE_BLOCK: // ✅ Safe to use with Shadow DOM import { Accordion, Card, Badge, Button, Tabs, Progress } from '@your-ui/components'; // ❌ Don't use with Shadow DOM (they use portals) import { Dialog, Popover, Tooltip, DropdownMenu, Sheet, AlertDialog } from '@your-ui/components'; COMMAND_BLOCK: import { cva } from 'class-variance-authority'; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-red-500 text-white hover:bg-red-600", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ); // Button component export const Button = ({ variant, size, children }) => { return ( <button className={buttonVariants({ variant, size })}> {children} </button> ); }; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { cva } from 'class-variance-authority'; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-red-500 text-white hover:bg-red-600", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ); // Button component export const Button = ({ variant, size, children }) => { return ( <button className={buttonVariants({ variant, size })}> {children} </button> ); }; COMMAND_BLOCK: import { cva } from 'class-variance-authority'; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-red-500 text-white hover:bg-red-600", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ); // Button component export const Button = ({ variant, size, children }) => { return ( <button className={buttonVariants({ variant, size })}> {children} </button> ); }; CODE_BLOCK: // tailwind.config.js module.exports = { content: [ './src/**/*.{ts,tsx}', // CRITICAL: Include your UI library './node_modules/@your-ui-lib/**/*.{ts,tsx}', ], // Force include commonly used variant classes safelist: [ // Primary variants 'bg-primary', 'text-primary-foreground', 'hover:bg-primary/90', // Destructive variants 'bg-red-500', 'bg-red-600', 'hover:bg-red-600', // Sizes 'h-9', 'h-10', 'h-11', 'px-3', 'px-4', 'px-8', // Add every possible variant combination... ], }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // tailwind.config.js module.exports = { content: [ './src/**/*.{ts,tsx}', // CRITICAL: Include your UI library './node_modules/@your-ui-lib/**/*.{ts,tsx}', ], // Force include commonly used variant classes safelist: [ // Primary variants 'bg-primary', 'text-primary-foreground', 'hover:bg-primary/90', // Destructive variants 'bg-red-500', 'bg-red-600', 'hover:bg-red-600', // Sizes 'h-9', 'h-10', 'h-11', 'px-3', 'px-4', 'px-8', // Add every possible variant combination... ], }; CODE_BLOCK: // tailwind.config.js module.exports = { content: [ './src/**/*.{ts,tsx}', // CRITICAL: Include your UI library './node_modules/@your-ui-lib/**/*.{ts,tsx}', ], // Force include commonly used variant classes safelist: [ // Primary variants 'bg-primary', 'text-primary-foreground', 'hover:bg-primary/90', // Destructive variants 'bg-red-500', 'bg-red-600', 'hover:bg-red-600', // Sizes 'h-9', 'h-10', 'h-11', 'px-3', 'px-4', 'px-8', // Add every possible variant combination... ], }; CODE_BLOCK: :root { --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; /* ... many more */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... dark theme values */ } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: :root { --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; /* ... many more */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... dark theme values */ } CODE_BLOCK: :root { --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; /* ... many more */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... dark theme values */ } CODE_BLOCK: // tailwind.config.js module.exports = { theme: { extend: { colors: { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, }, }, }, }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // tailwind.config.js module.exports = { theme: { extend: { colors: { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, }, }, }, }; CODE_BLOCK: // tailwind.config.js module.exports = { theme: { extend: { colors: { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, }, }, }, }; COMMAND_BLOCK: // This won't work as expected export const ThemedCard = () => { return ( <div className="bg-background text-foreground p-4"> <h2 className="text-primary font-bold">Title</h2> <p>Content here...</p> </div> ); }; const ThemedCardWC = r2wc(ThemedCard, { shadow: 'open' }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // This won't work as expected export const ThemedCard = () => { return ( <div className="bg-background text-foreground p-4"> <h2 className="text-primary font-bold">Title</h2> <p>Content here...</p> </div> ); }; const ThemedCardWC = r2wc(ThemedCard, { shadow: 'open' }); COMMAND_BLOCK: // This won't work as expected export const ThemedCard = () => { return ( <div className="bg-background text-foreground p-4"> <h2 className="text-primary font-bold">Title</h2> <p>Content here...</p> </div> ); }; const ThemedCardWC = r2wc(ThemedCard, { shadow: 'open' }); CODE_BLOCK: // styles.css (imported by your component) :host { /* Re-declare all theme variables */ --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; /* ... all other variables */ } @tailwind base; @tailwind components; @tailwind utilities; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // styles.css (imported by your component) :host { /* Re-declare all theme variables */ --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; /* ... all other variables */ } @tailwind base; @tailwind components; @tailwind utilities; CODE_BLOCK: // styles.css (imported by your component) :host { /* Re-declare all theme variables */ --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --primary: 221.2 83.2% 53.3%; /* ... all other variables */ } @tailwind base; @tailwind components; @tailwind utilities; COMMAND_BLOCK: // 1. A stats card const StatsCard = () => ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold text-gray-900">Total Users</h3> <p className="text-3xl font-bold text-primary mt-2">1,234</p> <p className="text-sm text-gray-600 mt-1">+12% from last month</p> </div> ); // 2. A data table (with dropdown menu) const DataTable = () => ( <div className="bg-white rounded-lg shadow"> <Table> {/* table content */} </Table> <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem>Delete</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> ); // 3. A settings dialog const SettingsDialog = () => ( <Dialog> <DialogTrigger asChild> <Button variant="outline">Settings</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Settings</DialogTitle> </DialogHeader> {/* form content */} </DialogContent> </Dialog> ); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // 1. A stats card const StatsCard = () => ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold text-gray-900">Total Users</h3> <p className="text-3xl font-bold text-primary mt-2">1,234</p> <p className="text-sm text-gray-600 mt-1">+12% from last month</p> </div> ); // 2. A data table (with dropdown menu) const DataTable = () => ( <div className="bg-white rounded-lg shadow"> <Table> {/* table content */} </Table> <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem>Delete</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> ); // 3. A settings dialog const SettingsDialog = () => ( <Dialog> <DialogTrigger asChild> <Button variant="outline">Settings</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Settings</DialogTitle> </DialogHeader> {/* form content */} </DialogContent> </Dialog> ); COMMAND_BLOCK: // 1. A stats card const StatsCard = () => ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold text-gray-900">Total Users</h3> <p className="text-3xl font-bold text-primary mt-2">1,234</p> <p className="text-sm text-gray-600 mt-1">+12% from last month</p> </div> ); // 2. A data table (with dropdown menu) const DataTable = () => ( <div className="bg-white rounded-lg shadow"> <Table> {/* table content */} </Table> <DropdownMenu> <DropdownMenuTrigger>Actions</DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem>Delete</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> ); // 3. A settings dialog const SettingsDialog = () => ( <Dialog> <DialogTrigger asChild> <Button variant="outline">Settings</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Settings</DialogTitle> </DialogHeader> {/* form content */} </DialogContent> </Dialog> ); CODE_BLOCK: // Simple, self-contained components - Cards - Badges - Progress bars - Accordions - Tabs - Buttons (non-portal variants) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Simple, self-contained components - Cards - Badges - Progress bars - Accordions - Tabs - Buttons (non-portal variants) CODE_BLOCK: // Simple, self-contained components - Cards - Badges - Progress bars - Accordions - Tabs - Buttons (non-portal variants) CODE_BLOCK: // Components with portals or complex interactions - Dialogs - Popovers - Tooltips - Dropdown menus - Context menus - Toast notifications Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Components with portals or complex interactions - Dialogs - Popovers - Tooltips - Dropdown menus - Context menus - Toast notifications CODE_BLOCK: // Components with portals or complex interactions - Dialogs - Popovers - Tooltips - Dropdown menus - Context menus - Toast notifications CODE_BLOCK: // Option 1: Selective Shadow DOM // Use Shadow DOM only for truly isolated components const CardWC = r2wc(Card, { shadow: 'open' }); const BadgeWC = r2wc(Badge, { shadow: 'open' }); // Skip Shadow DOM for interactive components const DialogWC = r2wc(Dialog, { shadow: null }); const DropdownWC = r2wc(Dropdown, { shadow: null }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Option 1: Selective Shadow DOM // Use Shadow DOM only for truly isolated components const CardWC = r2wc(Card, { shadow: 'open' }); const BadgeWC = r2wc(Badge, { shadow: 'open' }); // Skip Shadow DOM for interactive components const DialogWC = r2wc(Dialog, { shadow: null }); const DropdownWC = r2wc(Dropdown, { shadow: null }); CODE_BLOCK: // Option 1: Selective Shadow DOM // Use Shadow DOM only for truly isolated components const CardWC = r2wc(Card, { shadow: 'open' }); const BadgeWC = r2wc(Badge, { shadow: 'open' }); // Skip Shadow DOM for interactive components const DialogWC = r2wc(Dialog, { shadow: null }); const DropdownWC = r2wc(Dropdown, { shadow: null }); COMMAND_BLOCK: // Option 2: No Shadow DOM, CSS Modules // Use CSS Modules for scoping instead import styles from './Card.module.css'; const Card = () => ( <div className={styles.card}> {/* Use scoped CSS instead of Shadow DOM */} </div> ); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Option 2: No Shadow DOM, CSS Modules // Use CSS Modules for scoping instead import styles from './Card.module.css'; const Card = () => ( <div className={styles.card}> {/* Use scoped CSS instead of Shadow DOM */} </div> ); COMMAND_BLOCK: // Option 2: No Shadow DOM, CSS Modules // Use CSS Modules for scoping instead import styles from './Card.module.css'; const Card = () => ( <div className={styles.card}> {/* Use scoped CSS instead of Shadow DOM */} </div> ); CODE_BLOCK: // Option 3: Scoped Tailwind (advanced) // Generate component-specific Tailwind with prefixes // tailwind.config.js module.exports = { prefix: 'card-', // All classes become card-bg-white, card-p-4, etc. content: ['./src/Card.tsx'], }; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Option 3: Scoped Tailwind (advanced) // Generate component-specific Tailwind with prefixes // tailwind.config.js module.exports = { prefix: 'card-', // All classes become card-bg-white, card-p-4, etc. content: ['./src/Card.tsx'], }; CODE_BLOCK: // Option 3: Scoped Tailwind (advanced) // Generate component-specific Tailwind with prefixes // tailwind.config.js module.exports = { prefix: 'card-', // All classes become card-bg-white, card-p-4, etc. content: ['./src/Card.tsx'], }; - React components wrapped as web components - Styled with Tailwind CSS - Using shadcn/ui components for the UI - Wrapped with Shadow DOM for style encapsulation - Single Tailwind CSS file: ~50-100KB (minified) - With 3 web components: 150-300KB - With 10 web components: 500KB-1MB - DialogContent gets portaled to document.body - It's now outside your Shadow DOM - All your Tailwind classes (inside Shadow DOM) can't reach it - The dialog renders, but looks completely unstyled - No background overlay - No styling on the dialog box - Text isn't centered - Buttons look like plain HTML - Z-index issues (might render behind other elements) - You need to manually list every possible class combination - Easy to miss classes (leading to visual bugs) - Increases CSS bundle size (defeats purpose of JIT) - Need to update whenever UI library changes - Theme variables are duplicated everywhere - Dark mode requires extra work (can't just toggle a class on document.body) - Updating theme means updating multiple files - No single source of truth - All styles self-contained - Bundle: +80KB (Tailwind CSS) - Table looks good - Dropdown menu broken (portal renders unstyled outside Shadow DOM) - Bundle: +80KB (Tailwind CSS) - Button looks fine - Dialog content appears but completely unstyled - Backdrop might not work - Bundle: +80KB (Tailwind CSS) - All portals work correctly - Dropdown and dialog properly styled - Bundle: 80KB (single Tailwind CSS file) - No style encapsulation - Potential class name conflicts - Global styles can leak in/out - Need to be careful with specificity - Don't use portals - Don't need complex interactions outside their boundary - Benefit from style isolation - Bundle size matters - Duplicating Tailwind CSS across components gets expensive fast - Portals break Shadow DOM - Most modern UI libraries use portals heavily - CSS variables don't cross boundaries - Theming becomes complicated - CVA needs special handling - Dynamic classes require safelist configuration - There's always a tradeoff - Encapsulation vs. bundle size vs. functionality - Skip Shadow DOM entirely for our use case - Use TypeScript and component wrappers for type safety - Accept the global stylesheet - Let shadcn/ui portals work as intended - Focus on clear component APIs instead of Shadow DOM encapsulation - Shadow DOM Spec - Tailwind CSS - Radix UI Primitives - Class Variance Authority