網站導覽

遷移

從 CSS 與 CSS Modules 遷移

在 class 字串、token 或組合方式有助於維護時,將全域 CSS 與 CSS Modules 移向 Master CSS。

本頁目錄

一般 CSS 與 CSS Modules 可以依專案需要,長期與 Master CSS 並存。將遷移視為整理工作,不必要求每一個宣告都變成 class。

將重複、純視覺且適合由標記負責的樣式移到 Master CSS class。全域重設、第三方樣式、複雜選擇器、生成的 CSS 與難以閱讀的一次性規則,則保留在一般 CSS。


#遷移策略

選擇一個能檢查原始 CSS 與渲染狀態的小元件。依共用遷移流程建立基準並檢查每批工作;本指南著重判斷哪些宣告適合放進標記,哪些局部 CSS 契約應該保留。

理想的第一批工作有穩定的視覺宣告,不涉及第三方選擇器。若模組的局部 class 名稱是 API、需要選取內部結構,或替換後會讓複雜選擇器散落各使用處,就保留模組。

改變撰寫方式,保留結果

比較宣告與計算結果。token 名稱相似還不夠,值、配色與互動狀態都要一致。

文字說明
  1. 先確認值. CSS 規則提供 1rem 間距、1.5rem 內距與 0.5rem 圓角。
  2. 對應預設樣式集. gap-md、p-lg、r-lg 提供相同值,兩個版本使用相同表面與文字角色。
  3. 保留結果. 卡片的版面、內容與色彩維持不變,只改變樣式決策的撰寫位置。

#在既有 CSS 旁加入 Master CSS

Master CSS 開始為新標記或已轉換的標記生成規則時,繼續載入原本的 CSS。刪除局部規則前先檢查串接:未分層的舊宣告優先於一般分層 utility,即使 Master CSS 的匯入較晚也是如此。因此,同一元素同時保留兩個選擇器,可能掩蓋轉換是否成功。

app.css
@import "@master/css";

預設來源探索已足以應付多數專案。只有在遷移需要明確掃描邊界,或 class 未以完整字串出現在原始碼時,才新增 @source、@source not、@safelist 或 @blocklist。

大量轉換檔案前,先依渲染模式決定專案使用靜態、執行階段或漸進式渲染。


#將局部 CSS 轉為標記中的 class

如果 class 清單比獨立選擇器更容易審查,就把局部視覺宣告移到標記。此範例已使用安裝之預設樣式集的共用表面與文字角色,轉換時維持不變。

Card.module.css
.card {  display: grid;  gap: 1rem;  padding: 1.5rem;  border-radius: .5rem;  background: var(--color-surface-raised);  color: var(--color-text-body);}
Card.tsx
export function Card({ children }: { children: React.ReactNode }) {  return (    <section className="grid gap-md p-lg r-lg bg-surface-raised fg-text-body">      {children}    </section>  )}

以兩種樣式方式建立相同卡片

兩張卡片的間距、內距、圓角與背景相同。固定的 .5rem 圓角對應預設樣式集的 r-lg token。兩個版本使用相同的自適應表面與文字角色。

若值代表產品決策,優先使用 bg-surface-card、p-card 或 r-card 這類具名主題 token。若 CSS 屬性本身比新抽象更清楚,則使用 (scroll-margin-top:4rem) 這類直接的原生宣告 class。


#當 CSS Modules 是元件 API 時予以保留

元件提供穩定的局部 class 名稱、需要選取內部結構,或在局部變體中使用專案 token 時,CSS Modules 仍然有用。

Vite、Next.js 與 Webpack 會自動為局部樣式表提供專案主題定義。原生宣告會輸出所需變數,不必再次匯入應用程式的 CSS 輸出。

Button.module.css
.button {  display: inline-flex;  align-items: center;  justify-content: center;  border-radius: var(--radius-md);  gap: var(--spacing-xs);  padding-inline: var(--spacing-md);  background-color: var(--color-blue-60);  color: oklch(100% 0 none);  min-height: 2.5rem;}.button:disabled {  opacity: 0.5;  pointer-events: none;}
Button.tsx
import styles from './Button.module.css'export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {  const { className, type = 'button', ...buttonProps } = props  return <button className={[styles.button, className].filter(Boolean).join(' ')} type={type} {...buttonProps} />}

如果移除模組會迫使太多使用處同時修改,就採用這個模式。日後局部 class 不再有價值時,再移除模組。

上方模組未分層,因此其一般宣告優先於透過 className 傳入的分層 utility。若呼叫端應能以 utility 覆寫元件預設值,請將預設值放入 @layer components,並驗證串接結果。

此包裝元件預設使用 type="button";送出表單時請傳入 type="submit"。採用時,保留既有元件的 ref 轉送與事件契約。


#將共用值移到 @theme

大量轉換 class 前,通常應先將專案擁有的自訂屬性放進 @theme,讓標記使用 token 名稱,而非寫死的值。

app.css
@import "@master/css";@theme {  --color-surface-card: var(--color-surface-raised);  --color-body: var(--color-text-body);  --spacing-card: 1.5rem;  --radius-card: .5rem;}
<article class="grid r-card fg-body grid-cols(3)@md bg-surface-card p-card">  ...</article>

由執行階段管理、元件私有,或刻意不屬於設計系統的值,保留為一般 CSS 自訂屬性。

<div className="(width:var(--progress)) bg-blue-60" style={{ '--progress': `${value}%` } as React.CSSProperties} />

#驗證遷移結果

每批工作都要同時檢查程式碼與介面。

  • 執行專案的格式化、lint、型別檢查、測試與正式建置。
  • 使用程式碼檢查驗證 Master CSS class 字串。
  • 比較遷移畫面在明暗主題、響應式斷點、懸停與焦點狀態下的表現。
  • 若將選擇器、動畫或第三方輸出改成 class 字串後更難理解,則保留其 CSS。
  • 確認沒有元件或頁面依賴舊 CSS 後,才能移除。

#相關參考