網站導覽

遷移

從 CSS-in-JS 遷移

將 styled-components 與 Emotion 的靜態樣式轉為 Master CSS,保留動態值,並讓可重用元件的契約保持明確。

本頁目錄

CSS-in-JS 遷移通常應先搬移靜態視覺樣式,最後才處理執行階段行為。Styled Components 與 Emotion 可以留在專案中,讓 Master CSS 一次接手一組元件的 class 樣式。

本指南涵蓋 styled-components、@emotion/react 與 @emotion/styled。仍需要在執行階段生成選擇器或值的元件,不必移除 CSS-in-JS。


#遷移策略

依共用遷移流程建立基準,再拆分目前混合在各樣式包裝元件中的職責:

輸入移往何處元件保留的部分
靜態宣告完整標記 class 或 @layer componentsDOM 結構與事件處理函式
有限的視覺變體完整 class 字串對照表公開變體 prop 及其預設值
持續變動的值透過 style 傳入 CSS 自訂屬性測量、驗證與狀態
生成的選擇器或關鍵影格有明確替代方式前保留既有 CSS-in-JS建立它們的執行階段邏輯

替換包裝元件前,先檢查 as、ref、prop 過濾與 SSR 設定。樣式區塊很小的元件,仍可能有很大的公開契約。

一個 class,不同數值

以自訂屬性傳入變動資料。讓掃描器能看見完整 class 字串,狀態驗證則留在元件內。

文字說明
  1. 穩定 class. 兩個實例都請求 (width:var(--value)),不將百分比插入 class 名稱。
  2. 局部資料. 每個實例在自己的元素設定 --value,讓同一宣告產生不同寬度。
  3. 保留行為. 元件仍限制數值範圍,並提供對應的 aria-valuenow。

#轉換靜態樣式元件

從只套用穩定視覺樣式的包裝元件開始。

Button.tsx
import styled from 'styled-components'const Button = styled.button`  display: inline-flex;  align-items: center;  gap: .5rem;  min-height: 2.5rem;  padding: 0 1rem;  border-radius: .5rem;  background: ${({ theme }) => theme.colors.primary};  color: white;`

將可重用的值移到 @theme,再於渲染按鈕的地方使用 class 字串。

app.css
@import "@master/css";@theme {  --color-primary: #2563eb;}
Button.tsx
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {  const { className, type = 'button', ...buttonProps } = props  return (    <button      className={['inline-flex items-center gap-xs (min-height:2.5rem) py(0) px-md r-lg bg-primary fg-white', className].filter(Boolean).join(' ')}      type={type}      {...buttonProps}    />  )}

原本的 .5rem 圓角對應預設樣式集的 r-lg,而非 r-md。以具名 token 替換固定值時,請比較計算值。此包裝元件預設為 type="button";提交表單時傳入 type="submit"。請檢查既有表單的使用處,因為未設定 type 的原生按鈕預設會送出表單。

此範例替換的是視覺包裝元件。若要替換契約更廣的元件,請另外保留既有 ref 轉送、多型 as prop 與事件行為。

如果專案需要具名元件 class,保留上方 @theme 區塊,並在下方新增元件層:

app.css
@import "@master/css";@layer components {  .btn {    display: inline-flex;    align-items: center;    justify-content: center;    border-radius: var(--radius-lg);    gap: var(--spacing-xs);    padding-block: 0;    padding-inline: var(--spacing-md);    background-color: var(--color-primary);    color: oklch(100% 0 none);    min-height: 2.5rem;    &:hover {      background-color: var(--color-blue-70);    }    &:disabled { opacity: 0.5; }  }}
<button type="button" class="btn">Save</button>

#將變體轉為 class 對照表

避免用字串片段組合 Master CSS class 名稱。完整 class 字串最適合靜態渲染與 lint。

const toneClasses = {  neutral: 'bg-surface-base fg-text-body b-line-divider b-solid b(1)',  primary: 'bg-blue-60 fg-white bg-blue-70:hover',  danger: 'bg-red-70 fg-white bg-red-80:hover'}const sizeClasses = {  sm: '(min-height:2rem) px-sm font-sm',  md: '(min-height:2.5rem) px-md font-sm',  lg: '(min-height:3rem) px-lg font-md'}type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {  tone?: keyof typeof toneClasses  size?: keyof typeof sizeClasses}export function Button({ tone = 'neutral', size = 'md', className, type = 'button', ...props }: ButtonProps) {  return (    <button      className={['inline-flex items-center r-md', sizeClasses[size], toneClasses[tone], className].filter(Boolean).join(' ')}      type={type}      {...props}    />  )}

包裝元件會先取出 tone 與 size,再展開其餘原生 props。替換 styled-components 的暫態 props 或 shouldForwardProp 時,請保留過濾:樣式專用 props 不應傳到原生元素。如果舊公開 API 使用 $tone,請保留此拼法,直到有意識地遷移各使用處。

如果變體不只控制視覺樣式,就保留 prop,只遷移視覺宣告。使用最小高度,能讓標籤換行或放大,不會被固定高度限制。

附加 className 會保留呼叫端的 class,但不保證它們優先於衝突的 utility。HTML class 順序不決定串接。使用變體 props 選定一個值,或將可覆寫的預設值放入 @layer components,讓呼叫端 utility 可以覆寫。


#以 CSS 變數處理動態值

執行階段值不應變成生成的 class 名稱。保持 class 固定,以 CSS 變數傳入變動值。這與 Emotion 的動態樣式建議相同:變動值屬於元素,可重用宣告則維持穩定。

export function Meter({ value }: { value: number }) {  const percent = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0  return (    <div role="progressbar" aria-label="Progress" aria-valuemin={0} aria-valuemax={100} aria-valuenow={percent}      className="overflow-hidden (height:0.5rem) pill bg-surface-inset">      <div        className="h-full (width:var(--value)) bg-text-muted (transition-property:width) (transition-duration:var(--duration-fast))@motion-safe (transition-timing-function:var(--easing-smooth))"        style={{ '--value': `${percent}%` } as React.CSSProperties}      />    </div>  )}

在 25% 與 75% 時,內部元素仍使用 (width:var(--value)),只改變 --value。限制範圍後的數值也會提供給 aria-valuenow,讓視覺值與無障礙值一致。

如果元件必須依執行階段資料建立選擇器、關鍵影格或樣式區塊,且無法清楚以 CSS 變數表達,就保留 CSS-in-JS。


#謹慎搬移全域樣式

全域樣式常包含應用程式的基礎結構。請分成小批搬移。

GlobalStyles.tsx
import { Global, css } from '@emotion/react'export function GlobalStyles() {  return <Global styles={css`    body {      margin: 0;      background: var(--color-surface-base);    }  `} />}
app.css
@import "@master/css";@layer base {  body {    margin: 0;  }}@layer defaults {  body { background: var(--color-surface-base); }}

原先使用這些全域樣式的每個路由、shell 與伺服器渲染路徑都載入替代 CSS 進入點前,請保留 CSS-in-JS 全域樣式。


#驗證遷移結果

  • 每批工作後執行格式檢查、lint、型別檢查、測試與正式建置。
  • 若 CSS-in-JS 設定包含 SSR 擷取或 hydration 邏輯,請比較伺服器渲染輸出。
  • 檢查深色模式、響應式斷點、懸停、focus-visible、停用、選取與多型 as 狀態。
  • 確認所有剩餘元件都不再需要後,才移除 Babel 外掛、Emotion 快取設定、styled-components SSR 串接設定與套件相依性。

#相關參考