🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

最近在@sylwia-lask 發表的這篇文章《 2026 年學習 CSS 是否浪費時間? 》真的讓我感觸很深,尤其是關於可存取性會把你直接拉回原始 CSS 的那部分。

最近有了 Tailwind 和 shadcn,大多數樣式設定都…非常有效。快速操作,調整一兩個類,就搞定了。

然後,影子DOM出現了。

突然間,那些「理當正常運作」的功能都失效了。覆蓋設定不再生效,樣式變得難以駕馭,所有那些抽象概念都顯得比預期的要單薄得多。

這不是對 Tailwind 或 shadcn 的抱怨……只是想提醒大家,掌握 CSS 知識仍然能在事情出錯時幫助你。

夢想與現實

所以,我的想法是這樣的:用 React 建立美觀、可重複使用的 Web 元件,用 Tailwind CSS 設定樣式,用 shadcn/ui 打造精緻的 UI 元件,最後用 Shadow DOM 封裝起來,實現完美封裝。聽起來很棒,對吧?

嗯……並非如此。事實證明,這三種技術並不能很好地兼容。以下是我們付出慘痛代價後得出的結論。

簡而言之: Shadow DOM + Tailwind + shadcn/ui = 痛苦。請根據實際需求謹慎選擇,而非理論上的理想方案。有時,「不完美」的方案反而是正確的。

準備工作

我們正在建造:

  • React 元件封裝成 Web 元件

  • 使用Tailwind CSS進行樣式設計

  • 使用shadcn/ui元件建立使用者介面

  • 使用Shadow DOM進行樣式封裝

我們來談談哪裡出了問題。

問題一:Shadow DOM 與 Tailwind CSS-完全衝突

他們為什麼合不來

Tailwind CSS 的設計概念很簡單:在全域樣式表中使用實用類別。你只需要引入一個 CSS 文件,然後頁面上的每個元素就可以使用諸如bg-blue-500flex justify-center之類的類別。

Shadow DOM 的設計概念恰恰相反:完全隔離。 Shadow DOM 內部的樣式不會洩漏出去,外部的樣式也不會洩漏進來。這對於封裝性來說非常棒,但對於 Tailwind 框架來說卻很糟糕。

事情經過是這樣的:

// 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);

結果:元件渲染成功,但看起來完全錯亂。沒有內邊距,沒有背景色,沒有圓角。什麼都沒有。所有 Tailwind 類別都被忽略了,因為全域 Tailwind 樣式表無法穿透 Shadow DOM 邊界。

所謂的「解決方案」(此處加引號)

您必須將 Tailwind CSS 直接匯入到每個元件中:

// styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;

// Component file
import './styles.css'; // Import for every component

const MyCardWC = r2wc(MyCard, {
  shadow: 'open'
});

這種方法可行,但需要付出代價:

捆綁包尺寸爆炸

每個 Web 元件都包含自己完整的 Tailwind CSS 副本。如果頁面上有 5 個元件,那麼 Tailwind CSS 就會載入 5 次。這意味著 5 份完全相同的 CSS 被載入。

不使用瀏覽器快取

由於每個元件都有自己打包的樣式,因此無法利用瀏覽器快取來共享 CSS。每個元件下載包都包含相同的 Tailwind 工具。

建構複雜性

你的建置工具需要分別處理每個元件的 CSS 導入,這使得你的 webpack/vite 配置更加複雜。

實數:

  • 單一 Tailwind CSS 檔案:約 50-100KB(壓縮後)

  • 包含 3 個 Web 元件:150-300KB

  • 包含 10 個 Web 元件:500KB-1MB

是的,不太好。

問題二:shadcn/ui 和門戶問題

shadcn/ui 的特殊之處(以及問題所在)

shadcn/ui 是基於 Radix UI 的基本元件建構的,這些元件非常出色。但它們有一個與 Shadow DOM 不相容的怪癖:門戶(portals)

像是對話框、下拉式選單、彈出框和工具提示這樣的元件都使用 React Portal 來渲染其內容,使其位於常規元件樹之外,通常是透過追加到document.body來實現的。這對於 z-index 管理以及避免溢出問題來說很巧妙,但對於 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' });

如何運作:手風琴元件會將所有內容直接渲染到元件內部。沒有傳送門,也沒有內容瞬移。所有 HTML 都保留在元件樹內,因此 Shadow DOM 可以對其進行樣式設定。

例如:對話框崩潰

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' });

它為什麼會壞:

  1. DialogContent被傳送到document.body

  2. 它現在位於你的 Shadow DOM 之外。

  3. 你所有的 Tailwind 類別(在 Shadow DOM 內部)都無法存取它。

  4. 對話框可以渲染,但看起來完全沒有樣式。

你所看到的:

  • 無背景疊加

  • 對話框沒有樣式

  • 文字未居中

  • 按鈕看起來像是純 HTML 程式碼。

  • Z軸索引問題(可能渲染在其他元素後面)

變通方法

你必須二選一:影子DOM或傳送門。兩者不可兼得。

方案 A:對包含大量 portal 元件停用 Shadow DOM

// No Shadow DOM = portals work, but no encapsulation
const MyDialogWC = r2wc(MyDialog, {
  shadow: null
});

現在你需要全域管理樣式,並處理潛在的類別名稱衝突。

選項 B:僅使用非門戶元件

// ✅ 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';

這會大大限制你的使用者介面工具包。

問題 3:動態類別和 CVA

類別變更授權(CVA)的複雜性

shadcn/ui 使用 CVA 來處理元件變體——不同的尺寸、顏色和狀態。這會動態產生 Tailwind 類別:

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>
  );
};

影子DOM問題

所有這些動態產生的類別都需要存在於 Shadow DOM 的樣式表中。但是 Tailwind 的 JIT(即時)編譯器只會在建置時將檔案中找到的類別包含在內。

當 CVA 在執行時期動態合併類別時,Tailwind 可能沒有將它們包含在建置中,導致樣式缺失。

解決方法:將所有內容加入安全列表

// 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...
  ],
};

安全清單的問題:

  • 您需要手動列出所有可能的課程組合

  • 容易錯過課程(導致視覺錯誤)

  • 增加 CSS 包大小(違背了 JIT 的目的)

  • UI庫更改時需要更新

問題 4:主題變數與 CSS 自訂屬性

shadcn/ui 如何實現主題化

shadcn/ui 使用 CSS 自訂屬性(變數)進行主題化:

: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 */
}

然後在你的 Tailwind 配置中:

// 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))',
        },
      },
    },
  },
};

Shadow DOM 破壞了變數繼承

CSS 自訂屬性會透過 DOM 樹繼承,但 Shadow DOM 建立了一個邊界。在外部定義的變數不會自動流入。

// 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' });

結果:您的元件無法存取--background--foreground--primary變數。所有主題顏色將回退到預設值或完全失效。

解決方案:複製變數

您需要在 Shadow DOM 中重新聲明 CSS 變數:

// 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;

這種方法存在的問題:

  • 主題變數到處重複出現。

  • 深色模式需要額外的工作(不能簡單地在document.body上切換一個類別)。

  • 更新主題意味著更新多個文件

  • 沒有單一的真理來源

實際影響:個案研究

讓我們來看看這在實踐中意味著什麼。假設您正在使用以下元件建立儀表板:

// 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>
);

啟用 Shadow DOM 後:

統計卡: ✅ 完美執行

  • 沒有門戶

  • 所有款式均為獨立式

  • 捆綁包:+80KB(Tailwind CSS)

資料表: ⚠️ 部分損壞

  • 桌子看起來不錯

  • 下拉式選單顯示異常(在 Shadow DOM 之外渲染的 Portal 沒有樣式)

  • 捆綁包:+80KB(Tailwind CSS)

設定對話框: ❌ 完全損壞

  • 按鈕看起來沒問題

  • 對話方塊內容顯示,但完全沒有樣式。

  • 背景布幕可能無法正常運作

  • 捆綁包:+80KB(Tailwind CSS)

總捆綁成本: 3 個元件共 240KB 重複 CSS

不使用 Shadow DOM:

一切正常:

  • 所有入口網站均運作正常。

  • 下拉式選單和對話框樣式正確

  • 檔案大小:80KB(單一 Tailwind CSS 檔案)

但:

  • 無樣式封裝

  • 潛在的類別名稱衝突

  • 全域樣式可以傳入/傳出

  • 需要注意具體細節。

那麼答案是什麼呢?

Shadow DOM 何時適用:

良好應用案例:

// Simple, self-contained components
- Cards
- Badges
- Progress bars
- Accordions
- Tabs
- Buttons (non-portal variants)

這些元件:

  • 不要使用傳送門

  • 無需在其邊界之外進行複雜的交互

  • 受益於風格隔離

何時跳過 Shadow DOM:

跳過它:

// 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 和 shadcn/ui 本身都是非常優秀的技巧。但是,如果把它們放在一起呢?它們就會互相衝突。

Shadow DOM 需要:完全隔離

Tailwind 需要:全域實用程式類

shadcn/ui 需要:用於正確管理 z-index 的門戶

只能選兩個。三個不可能同時完美運作。

我們學到了什麼:

  1. 打包大小很重要-在元件間重複使用 Tailwind CSS 會迅速增加成本。

  2. Portal 會破壞 Shadow DOM——大多數現代 UI 函式庫都大量使用 Portal。

  3. CSS變數不會跨越邊界——主題化變得複雜

  4. CVA 需要特殊處理- 動態類別需要安全清單配置

  5. 總是會有權衡取捨——封裝性、套件大小和功能性之間需要權衡。

我們採取的措施是:

我們最終採用了混合方法

  • 在我們的用例中完全跳過 Shadow DOM。

  • 使用 TypeScript 和元件包裝器來實現類型安全

  • 接受全域樣式表

  • 讓 shadcn/ui 入口網站按預期執行

  • 專注於清晰的元件 API,而不是 Shadow DOM 封裝。

它完美嗎?不。但它有效,而這比建築的純粹性更重要。

資源


原文出處:https://dev.to/ujja/i-know-this-will-upset-some-devs-but-tailwind-shadcnui-shadow-dom-pain-44l7


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝5   💬7  
121
🥈
我愛JS
📝1   💬3   ❤️2
54
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付