跳到主要內容

同一份設計,跑在 Svelte / React / Vue / Next

為什麼是純 CSS 變數而不是 Tailwind、四個框架的模板對照表,以及移植時的逐項檢查。

約 6 分鐘 · framework-portability.md

1. 為什麼不用 Tailwind

Tailwind 很好用,但這套系統要同時活在四個框架的專案裡,而 Tailwind 帶來三個移植障礙:

  1. 設定檔綁定tailwind.config.js 要進你的專案,跟你既有的設定合併
  2. 建置步驟綁定 — 換風格要重跑 build,不能只換一個檔案
  3. 看不見的設計決策p-4 gap-6 rounded-2xl 讀不出設計意圖,而且無法在執行期切換

CSS 自訂屬性是四個框架原生共同支援的唯一語言。它在執行期就能換、不需要建置、複製貼上就會動。

代價是要自己寫 CSS。對一個設計系統來說,這是應該的 —— 設計系統的產出本來就該是 CSS。

2. 移植的三個部分

tokens.css        原封不動      ← 四個框架都一樣
component.css     原封不動      ← 只要 class 名稱不變
模板              需要轉換      ← 唯一要動的東西

所以移植的工作量只有第三項,而且都是機械式的替換。

3. 模板對照表

Svelte React / Next Vue 3
class="x" className="x" class="x"
for="id" htmlFor="id" for="id"
onclick={fn} onClick={fn} @click="fn"
oninput={fn} onChange={fn} @input="fn"
bind:value={x} value={x} onChange={e => setX(e.target.value)} v-model="x"
bind:checked={x} checked={x} onChange={...} v-model="x"
{#if c}…{/if} {c && …} v-if="c"
{#if c}…{:else}…{/if} {c ? … : …} v-if / v-else
{#each xs as x (x.id)} {xs.map(x => <… key={x.id}>)} v-for="x in xs" :key="x.id"
{value} {value} {{ value }}
{@html s} dangerouslySetInnerHTML={{__html: s}} v-html="s"
let x = $state(0) const [x, setX] = useState(0) const x = ref(0)
$derived(a + b) a + b(直接算) computed(() => a + b)
$effect(() => …) useEffect(() => …, [deps]) watchEffect(() => …)
<style>(自動 scoped) 匯入 component.css <style scoped>

4. 各框架的注意事項

React

  • classclassNameforhtmlFor。忘記的話樣式整個不見,而且 console 只給一行警告
  • 每個 map 出來的元素要 key,而且要用穩定的 id,不要用 index(見 forms-focus-input
  • 自閉合標籤<br /><img /><input />
  • inline style 是物件style={{ width: '50%' }}
  • CSS 變數在 inline style 要用字串 keystyle={{ ['--x' as string]: value }}

Next.js(App Router)

  • 預設是 Server Component。有 useState、事件處理、瀏覽器 API 的檔案,第一行加 "use client"
  • CSS 在 app/layout.tsx import,順序是 contract → components → tokens
  • 圖片可以用 next/image,但要記得它會自動加 lazy,首屏圖片要 priority
  • 字型用 next/font 可以避免 CLS

Vue 3

  • <style scoped> 的 scope 機制跟 Svelte 類似,但深層選擇器要用 :deep()
  • v-html 的內容一樣要淨化
  • v-for 的 key 要放在 v-for 所在的那個元素上
  • 事件修飾符(@click.prevent)可以取代手寫 preventDefault

純 HTML

  • {#each} / {#if} 展開成靜態標記,class 名稱一個字都不要改
  • 需要互動的地方用最小量的 vanilla JS
  • 原生 <dialog><details> 幾乎涵蓋你需要的互動

5. 元件的 CSS 怎麼搬

Svelte 的 <style> 是編譯期 scoped,抽出來會失去隔離。本站的下載包已經幫你處理好:每個模組附一份 component.css,所有選擇器都加上了包裹 class。

/* 原本(Svelte scoped) */
.hero { … }
.wordmark { … }

/* 匯出後 */
.px-landing-hero-illustration .hero { … }
.px-landing-hero-illustration .wordmark { … }

用法:把區塊包在 <div class="px-landing-hero-illustration"> 裡。

@keyframes 不會被加前綴(加了會壞),所以動畫名稱要夠獨特。

6. 移植檢查清單

搬完一個區塊,逐項確認:

  • 所有 class 名稱跟原本完全一致(改了名字,CSS 就對不上)
  • data-* 屬性都保留了(變體是靠它們)
  • 迴圈都有穩定的 key
  • 事件名稱轉換正確
  • 表單的 label / for 關係還在
  • aria-* 屬性都保留了
  • CSS 三個檔案的 import 順序正確
  • 換一個 tokens.css 版面不會壞
  • 360 / 768 / 1440 三個寬度都試過

最後兩項是最容易漏、也最能證明搬對了的。

7. 讓 AI 幫你搬

本站每個模組頁都可以選目標框架,產生的 prompt 會自動帶上對應的轉換規則。做法:

  1. 在模組頁選框架
  2. 「下載檔案包」拿到 Svelte 原始碼 + 去 scope 化的 CSS
  3. 「複製完整 Prompt」
  4. 貼給 Claude Code,把資料夾一起給它

prompt 裡已經包含上面那張對照表的相關條目,以及移植檢查清單。

顯示設定

這裡改的每一項,會即時套用到站上所有預覽。

風格

密度

圓角

動態

系統層級的「減少動態效果」永遠優先於這裡的設定。

語言