Insights, tutorials, and updates by @Nosleepguy

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.
We were building:
React components wrapped as web components
Styled with Tailwind CSS
Using shadcn/ui components for the UI
Wrapped with Shadow DOM for style encapsulation
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.
Here's what happens:
// 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);
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:
// styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// Component file
import './styles.css'; // Import for every component
const MyCardWC = r2wc(MyCard, {
shadow: 'open'
});
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.
Real numbers:
Single Tailwind CSS file: ~50-100KB (minified)
With 3 web components: 150-300KB
With 10 web components: 500KB-1MB
Yeah, not great.
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.
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' });
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.
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' });
Why it breaks:
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
What you see:
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 have to choose: Shadow DOM or portals. Can't have both.
Option A: Disable Shadow DOM for portal-heavy components
// No Shadow DOM = portals work, but no encapsulation
const MyDialogWC = r2wc(MyDialog, {
shadow: null
});
Now you need to manage styles globally and deal with potential class name conflicts.
Option B: Only use non-portal components
// ✅ 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';
This limits your UI toolkit significantly.
shadcn/ui uses CVA to handle component variants - different sizes, colors, and states. This generates Tailwind classes dynamically:
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>
);
};
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.
// 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...
],
};
The problem with safelist:
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
shadcn/ui uses CSS custom properties (variables) for theming:
: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 */
}
Then in your Tailwind config:
// 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))',
},
},
},
},
};
CSS custom properties inherit through the DOM tree, but Shadow DOM creates a boundary. Variables defined outside don't automatically flow in.
// 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' });
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:
// 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;
Problems with this approach:
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
Let's look at what this means in practice. Say you're building a dashboard with these components:
// 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>
);
StatsCard: ✅ Works perfectly
No portals
All styles self-contained
Bundle: +80KB (Tailwind CSS)
DataTable: ⚠️ Partially broken
Table looks good
Dropdown menu broken (portal renders unstyled outside Shadow DOM)
Bundle: +80KB (Tailwind CSS)
SettingsDialog: ❌ Completely broken
Button looks fine
Dialog content appears but completely unstyled
Backdrop might not work
Bundle: +80KB (Tailwind CSS)
Total bundle cost: 240KB of duplicated CSS for 3 components
Everything works: ✅
All portals work correctly
Dropdown and dialog properly styled
Bundle: 80KB (single Tailwind CSS file)
But:
No style encapsulation
Potential class name conflicts
Global styles can leak in/out
Need to be careful with specificity
Good use cases:
// Simple, self-contained components
- Cards
- Badges
- Progress bars
- Accordions
- Tabs
- Buttons (non-portal variants)
These components:
Don't use portals
Don't need complex interactions outside their boundary
Benefit from style isolation
Skip it for:
// Components with portals or complex interactions
- Dialogs
- Popovers
- Tooltips
- Dropdown menus
- Context menus
- Toast notifications
// 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 });
// 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>
);
// 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'],
};
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.
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
We ended up with a hybrid approach:
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
Is it perfect? No. But it works, and that matters more than architectural purity.


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.
We were building:
React components wrapped as web components
Styled with Tailwind CSS
Using shadcn/ui components for the UI
Wrapped with Shadow DOM for style encapsulation
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.
Here's what happens:
// 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);
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:
// styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// Component file
import './styles.css'; // Import for every component
const MyCardWC = r2wc(MyCard, {
shadow: 'open'
});
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.
Real numbers:
Single Tailwind CSS file: ~50-100KB (minified)
With 3 web components: 150-300KB
With 10 web components: 500KB-1MB
Yeah, not great.
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.
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' });
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.
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' });
Why it breaks:
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
What you see:
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 have to choose: Shadow DOM or portals. Can't have both.
Option A: Disable Shadow DOM for portal-heavy components
// No Shadow DOM = portals work, but no encapsulation
const MyDialogWC = r2wc(MyDialog, {
shadow: null
});
Now you need to manage styles globally and deal with potential class name conflicts.
Option B: Only use non-portal components
// ✅ 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';
This limits your UI toolkit significantly.
shadcn/ui uses CVA to handle component variants - different sizes, colors, and states. This generates Tailwind classes dynamically:
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>
);
};
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.
// 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...
],
};
The problem with safelist:
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
shadcn/ui uses CSS custom properties (variables) for theming:
: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 */
}
Then in your Tailwind config:
// 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))',
},
},
},
},
};
CSS custom properties inherit through the DOM tree, but Shadow DOM creates a boundary. Variables defined outside don't automatically flow in.
// 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' });
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:
// 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;
Problems with this approach:
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
Let's look at what this means in practice. Say you're building a dashboard with these components:
// 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>
);
StatsCard: ✅ Works perfectly
No portals
All styles self-contained
Bundle: +80KB (Tailwind CSS)
DataTable: ⚠️ Partially broken
Table looks good
Dropdown menu broken (portal renders unstyled outside Shadow DOM)
Bundle: +80KB (Tailwind CSS)
SettingsDialog: ❌ Completely broken
Button looks fine
Dialog content appears but completely unstyled
Backdrop might not work
Bundle: +80KB (Tailwind CSS)
Total bundle cost: 240KB of duplicated CSS for 3 components
Everything works: ✅
All portals work correctly
Dropdown and dialog properly styled
Bundle: 80KB (single Tailwind CSS file)
But:
No style encapsulation
Potential class name conflicts
Global styles can leak in/out
Need to be careful with specificity
Good use cases:
// Simple, self-contained components
- Cards
- Badges
- Progress bars
- Accordions
- Tabs
- Buttons (non-portal variants)
These components:
Don't use portals
Don't need complex interactions outside their boundary
Benefit from style isolation
Skip it for:
// Components with portals or complex interactions
- Dialogs
- Popovers
- Tooltips
- Dropdown menus
- Context menus
- Toast notifications
// 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 });
// 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>
);
// 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'],
};
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.
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
We ended up with a hybrid approach:
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
Is it perfect? No. But it works, and that matters more than architectural purity.
![[Draw my life] | Ep 1: Introduction](https://res.cloudinary.com/nmhung/image/upload/f_auto,c_limit,w_3840,q_auto/v1770628231/blog/wt05wxceh9wmmvsunk26.jpg)
Episode 1: The Opening Chapter
Hi folks,
Allow me to welcome you to the beginning of my series, Draw My Life. This is not merely a recounting of events, but a journey through the thoughts, struggles, and lessons that shaped me from toddler to now. What it is, and why it exists, is simple: to share, to connect, and perhaps to inspire.
First of all, I deeply appreciate your time. Let’s break the ice between us.
I’m Harry—better known as Nosleepguy—a developer with an enduring passion for technology. My university years were spent immersed in Information Technology engineering, where I discovered my strongest skill: crafting elegant, intuitive front-end experiences. Today, I work at an IT outsourcing company, building solutions for clients across the globe.
But my story does not begin with code. I was born into a poor family in a small city near Ha Noi. No wealth, no privileges, no shortcuts—only the raw reality of life pressing forward. From those modest beginnings, every step became a test, every challenge a teacher.
This series exists for one purpose: to share my experiences, my knowledge, and the ways life has tested me. I want to show that resilience is not reserved for the privileged. If I can endure, learn, and grow, then so can you. My hope is that these words will carry more than memory—they will carry encouragement, a reminder that even in hardship, there is strength waiting to be found.
So here we begin. Episode 1 is not just an introduction; it is the opening of a dialogue between my past and your present. Walk with me, and let’s uncover the story together.
Catch you soon!