網站導覽

Master CSS 2.0

用可組合的 class、以 CSS 定義的設計 token,以及能讀取設計系統的工具來打造介面。

柔和的金色背景上,映著帶有光澤的黑曜石質感 2.0 字樣。

用 CSS 寫出自己的設計語言。 Master CSS 2.0 把可組合的 class、專案 token 和可重用的樣式接上同一套工具。用一個 class 描述響應式狀態,在樣式表裡定義主題,開發時隨手檢查生成的 CSS。

觀看影片: Master CSS 2.0 — Your project, understood.
本頁內容

來看看 Fieldwork,一份用這套方法做成的小刊物。

把構想做成網頁

查看 HTML 和 CSS

在應用的樣式表中,把 CSS 加在 @import '@master/css' 後面。依原始碼所示路徑放置素材,或將 HTML 和 @font-face 的 URL 換成自己的位置。

下載 · 白橡木室內照片 · 陶器習作照片 · Geist 可變字型

@font-face {  font-family: 'Geist';  font-style: normal;  font-weight: 100 900;  font-display: swap;  src: url('/fonts/GeistVariable.woff2') format('woff2');}@theme {  --color-brand: light-dark(#665442, #d7c2a6);  --spacing-story: 1.5rem;}@theme {  --font-sans: 'Geist';}@layer components {  .journal { font-family: var(--font-family-sans); font-size: 13px; line-height: 1.6; font-weight: 350; }  .journal :where(h2, h3, p) { margin: 0; }  .journal a { color: inherit; text-decoration: none; }  .journal a:hover { text-decoration: underline; text-underline-offset: .2em; }  .journal :focus-visible { outline: 2px solid var(--color-brand); outline-offset: 4px; }  .journal-head { padding: 20px 24px; border-bottom: 1px solid var(--color-line-subtle); }  .journal-brand { font-size: 25px; line-height: 1; font-weight: 500; letter-spacing: -.06em; }  .journal-edition { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; }  .journal-nav { display: flex; flex-wrap: wrap; gap: 12px 20px; font-size: 12px; }  .journal-main { min-width: 0; }  .journal-photo { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 4px; outline: 1px solid light-dark(rgb(0 0 0 / .08), rgb(255 255 255 / .08)); outline-offset: -1px; }  .journal-kicker { font-size: 11px; line-height: 1.5; letter-spacing: .09em; text-transform: uppercase; }  .journal-title { max-width: 12ch; font-size: clamp(28px, 4.8vw, 36px); line-height: 1.08; font-weight: 400; letter-spacing: -.04em; }  .journal-deck { max-width: 44ch; font-size: 14px; line-height: 1.7; }  .journal-meta { font-size: 11px; line-height: 1.5; }  .journal-read { border-top: 1px solid var(--color-line-subtle); padding-top: 4px; }  .journal-read > summary { width: fit-content; padding: 10px 0; cursor: pointer; font-size: 13px; }  .journal-copy { display: grid; gap: 12px; padding-top: 6px; font-size: 14px; line-height: 1.8; }  .journal-aside { min-width: 0; border-top: 1px solid var(--color-line-subtle); }  .journal-aside-title { font-size: 23px; line-height: 1.2; letter-spacing: -.03em; font-weight: 400; }  .journal-story { border-top: 1px solid var(--color-line-subtle); }  .journal-story > summary { display: flex; align-items: start; justify-content: space-between; gap: 12px; padding-block: 20px; cursor: pointer; list-style: none; }  .journal-story > summary::-webkit-details-marker { display: none; }  .journal-story-title { margin-top: 7px; font-size: 16px; line-height: 1.4; font-weight: 400; letter-spacing: -.015em; }  .journal-story-image { display: block; width: 64px; height: 76px; object-fit: cover; border-radius: 3px; }  .journal-story-arrow { font-size: 20px; line-height: 1; }  .journal-story[open] .journal-story-arrow { transform: rotate(45deg); }  .journal-story > p { padding-bottom: 20px; font-size: 13px; line-height: 1.8; }  .journal-about { display: grid; gap: 8px; padding: 20px 24px; border-top: 1px solid var(--color-line-subtle); font-size: 12px; line-height: 1.7; }  .journal-about p { max-width: 48ch; }  @media (width >= 38rem) {    .journal-grid { display: grid; grid-template-columns: 1.6fr 1fr; }    .journal-aside { border-top: 0; border-left: 1px solid var(--color-line-subtle); }    .journal-about { grid-template-columns: 1fr auto; align-items: center; gap: 24px; }  }  @media (width < 24rem) {    .journal-head { padding: 20px; }    .journal-edition-label { display: none; }    .journal-nav { gap: 16px; }  }}
產生的 CSS
@font-face {  font-family: Geist;  font-style: normal;  font-weight: 100 900;  font-display: swap;  src: url("/fonts/GeistVariable.woff2") format("woff2");}@layer components {  .journal {    font-family: var(--font-family-sans);    font-size: 13px;    font-weight: 350;    line-height: 1.6;  }  .journal :where(h2, h3, p) {    margin: 0;  }  .journal a {    color: inherit;    text-decoration: none;  }  .journal a:hover {    text-underline-offset: .2em;    text-decoration: underline;  }  .journal :focus-visible {    outline: 2px solid var(--color-brand);    outline-offset: 4px;  }  .journal-head {    border-bottom: 1px solid var(--color-line-subtle);    padding: 20px 24px;  }  .journal-brand {    letter-spacing: -.06em;    font-size: 25px;    font-weight: 500;    line-height: 1;  }  .journal-edition {    letter-spacing: .1em;    text-transform: uppercase;    font-size: 10px;  }  .journal-nav {    flex-wrap: wrap;    gap: 12px 20px;    font-size: 12px;    display: flex;  }  .journal-main {    min-width: 0;  }  .journal-photo {    aspect-ratio: 16 / 9;    object-fit: cover;    outline-offset: -1px;    border-radius: 4px;    outline: 1px solid light-dark(#00000014, #ffffff14);    width: 100%;    height: auto;    display: block;  }  .journal-kicker {    letter-spacing: .09em;    text-transform: uppercase;    font-size: 11px;    line-height: 1.5;  }  .journal-title {    letter-spacing: -.04em;    max-width: 12ch;    font-size: clamp(28px, 4.8vw, 36px);    font-weight: 400;    line-height: 1.08;  }  .journal-deck {    max-width: 44ch;    font-size: 14px;    line-height: 1.7;  }  .journal-meta {    font-size: 11px;    line-height: 1.5;  }  .journal-read {    border-top: 1px solid var(--color-line-subtle);    padding-top: 4px;  }  .journal-read > summary {    cursor: pointer;    width: fit-content;    padding: 10px 0;    font-size: 13px;  }  .journal-copy {    gap: 12px;    padding-top: 6px;    font-size: 14px;    line-height: 1.8;    display: grid;  }  .journal-aside {    border-top: 1px solid var(--color-line-subtle);    min-width: 0;  }  .journal-aside-title {    letter-spacing: -.03em;    font-size: 23px;    font-weight: 400;    line-height: 1.2;  }  .journal-story {    border-top: 1px solid var(--color-line-subtle);  }  .journal-story > summary {    cursor: pointer;    justify-content: space-between;    align-items: start;    gap: 12px;    padding-block: 20px;    list-style: none;    display: flex;  }  .journal-story > summary::-webkit-details-marker {    display: none;  }  .journal-story-title {    letter-spacing: -.015em;    margin-top: 7px;    font-size: 16px;    font-weight: 400;    line-height: 1.4;  }  .journal-story-image {    object-fit: cover;    border-radius: 3px;    width: 64px;    height: 76px;    display: block;  }  .journal-story-arrow {    font-size: 20px;    line-height: 1;  }  .journal-story[open] .journal-story-arrow {    transform: rotate(45deg);  }  .journal-story > p {    padding-bottom: 20px;    font-size: 13px;    line-height: 1.8;  }  .journal-about {    border-top: 1px solid var(--color-line-subtle);    gap: 8px;    padding: 20px 24px;    font-size: 12px;    line-height: 1.7;    display: grid;  }  .journal-about p {    max-width: 48ch;  }  @media (width >= 38rem) {    .journal-grid {      grid-template-columns: 1.6fr 1fr;      display: grid;    }    .journal-aside {      border-top: 0;      border-left: 1px solid var(--color-line-subtle);    }    .journal-about {      grid-template-columns: 1fr auto;      align-items: center;      gap: 24px;    }  }  @media (width < 24rem) {    .journal-head {      padding: 20px;    }    .journal-edition-label {      display: none;    }    .journal-nav {      gap: 16px;    }  }}@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-line-subtle: light-dark(oklch(0% 0 none / .06), oklch(100% 0 none / .06));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-family-sans: var(--font-sans, ui-sans-serif), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";    --radius-lg: .5rem;    --spacing-sm: .75rem;    --spacing-md: 1rem;    --spacing-lg: 1.5rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }  :root,  :host {    --color-brand: light-dark(#665442, #d7c2a6);    --spacing-story: 1.5rem  }  :root,  :host {    --font-sans: "Geist"  }}@layer utilities {  .items-center {    align-items: center  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .r-lg {    border-radius: var(--radius-lg)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-brand {    color: var(--color-brand)  }  .fg-text-body {    color: var(--color-text-body)  }  .fg-text-muted {    color: var(--color-text-muted)  }  .fg-text-strong {    color: var(--color-text-strong)  }  .block {    display: block  }  .flex {    display: flex  }  .grid {    display: grid  }  .flex-wrap {    flex-wrap: wrap  }  .gap-md {    gap: var(--spacing-md)  }  .gap-sm {    gap: var(--spacing-sm)  }  .justify-between {    justify-content: space-between  }  .overflow-hidden {    overflow: hidden  }  .p-story {    padding: var(--spacing-story)  }  .pb-lg {    padding-bottom: var(--spacing-lg)  }}
調整刊物寬度、切換主題,或打開一篇文章。範例使用 Master CSS 與原生 CSS,照片為此範例生成。

縮窄預覽,側欄會接在主文後面。打開一篇文章,版面會為內容騰出空間。切換主題時,重點色隨之改變,共用的間距則讓閱讀節奏一致。預覽裡也可以查看 HTML 和 CSS。

#在一個 class 裡表達更多

Master CSS 讓樣式決策跟著元素走。用 flex 和 grid 排版,用 gap-md 和 p-lg 套用專案 token,再把選擇器和條件寫在它們所限定的 class 上。

<a class="inline-flex px-md py-xs r-md fg-blue-60 fg-blue-70:hover@sm"   href="/journal">  Read the journal</a>

從左到右讀 fg-blue-70:hover@sm。檢視區寬度達到 sm 斷點,而且游標停在元素上時,就會套用 blue-70 色彩 token。預設門檻是 52.125rem。Master CSS 生成規則,瀏覽器則判斷選擇器和媒體查詢是否符合。

在一個 class 裡寫清楚條件

這個 class 把值、選擇器和條件寫在一起。瀏覽器會判斷生成的選擇器和媒體查詢是否符合。

文字說明
  1. fg-blue-70 選用 color: var(--color-blue-70)。:hover 將滑鼠停留狀態套在帶有這個 class 的元素上。
  2. @sm 使用預設媒體條件 (width >= 52.125rem)。查詢條件和 hover 狀態必須同時符合。

可以依要表達的樣式選擇寫法。

Class含義
p-lg使用 --spacing-lg token 設定內距。
m(0)外距為零。
p(16)內距為 16px。
grow(1)Flex 伸展比例為 1,沒有單位。
grid-cols(3)三個等寬的 Grid 欄位。
(aspect-ratio:4/3)原生 CSS 屬性和值。

數值函式的單位取決於對應的屬性。p(16) 是長度,grow(1) 則沒有單位。原生宣告也接受 CSS 函式和自訂屬性。使用 (width:var(--progress)) 時,JavaScript 可以更新變數值,class 本身維持相同。

更多組合 class 的方式
Class含義
fg-blue-60/.5 · -mt-sm將色彩不透明度設為 50%,或使用負值的間距 token。
fg-blue-60:hover>a · fg-blue-60_a選取游標停留在父元素上時的直屬連結,或選取後代連結。
grid@supports(display:grid)以原生功能查詢包住規則。
(opacity:0)@starting-style設定 CSS 轉場的起始不透明度。
p-md!將內距宣告標為 !important。

:focus-visible、:has() 和 ::before 等選擇器可以搭配 @media(...)、@supports(...) 和 @container(...)。多個條件會彼此巢狀,必須全部符合。生成的結果是可以直接檢查的一般 CSS。

想動手試試,可以從語法教學開始。

#建立自己的設計系統

先用預設的色彩、間距、排版、圓角、陰影和動態效果 token。介面逐漸有了自己的風格,再用 @theme 為專案的設計決策命名。

#廣色域色盤

十七組色相,每組十一個色階。預設色盤使用 OKLCH,有些色彩超出 sRGB,延伸至 Display P3 色域。每欄由淺至深排列,實際可見的範圍取決於螢幕。

從紅色到深紅色共有十七組色相,每欄由淺至深排列十一個色階。OKLCH 在相容的螢幕上能呈現廣色域色彩,實際可見範圍取決於螢幕。

可以用 fg-blue-50 或 bg-green-90 直接選用色階,也可以依用途命名。Fieldwork 共用同一個重點色和文章內距。請將下方的定義加在應用樣式表的 @import '@master/css' 後面。

用自己的配色呈現一篇短文

@theme {  --color-brand: light-dark(#665442, #d7c2a6);  --spacing-story: 1.5rem;}
產生的 CSS
@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-40: oklch(68.6% .0028 265);    --color-zinc-60: oklch(48.6% .003 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265);    --color-zinc-100: oklch(15% .0016 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30));    --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white));    --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --font-weight-regular: 400;    --font-size-xs: .75rem;    --font-size-sm: .875rem;    --font-size-2xl: 1.5rem;    --text-xs: var(--font-size-xs);    --text-xs--line-height: 1.5;    --text-xs--letter-spacing: 0;    --text-sm: var(--font-size-sm);    --text-sm--line-height: 1.5;    --text-sm--letter-spacing: 0;    --text-2xl: var(--font-size-2xl);    --text-2xl--line-height: 1.35;    --text-2xl--letter-spacing: -.015em;    --radius-lg: .5rem;    --spacing-xs: .5rem;    --spacing-sm: .75rem;    --container-sm: 24rem  }  :root,  :host {    --shadow-sm: 0 0 0 1px light-dark(oklch(0% 0 none / .04), oklch(100% 0 none / .05)), 0 1px 1px -.5px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .3)), 0 2px 2px -1px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .26)), 0 4px 4px -2px light-dark(oklch(0% 0 none / .04), oklch(0% 0 none / .24))  }  :root,  :host {    --color-brand: light-dark(#665442, #d7c2a6);    --spacing-story: 1.5rem  }}@layer utilities {  .text-2xl {    font-size: var(--text-2xl);    line-height: var(--text-2xl--line-height, normal);    letter-spacing: var(--text-2xl--letter-spacing, normal)  }  .text-sm {    font-size: var(--text-sm);    line-height: var(--text-sm--line-height, normal);    letter-spacing: var(--text-sm--letter-spacing, normal)  }  .text-xs {    font-size: var(--text-xs);    line-height: var(--text-xs--line-height, normal);    letter-spacing: var(--text-xs--letter-spacing, normal)  }  .bg-surface-raised {    background-color: var(--color-surface-raised)  }  .r-lg {    border-radius: var(--radius-lg)  }  .shadow-sm {    box-shadow: var(--shadow-sm)  }  .fg-brand {    color: var(--color-brand)  }  .fg-text-body {    color: var(--color-text-body)  }  .fg-text-muted {    color: var(--color-text-muted)  }  .fg-text-strong {    color: var(--color-text-strong)  }  .grid {    display: grid  }  .font-regular {    font-weight: var(--font-weight-regular)  }  .gap-sm {    gap: var(--spacing-sm)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .mx-auto {    margin-inline: auto  }  .max-w-sm {    max-width: var(--container-sm)  }  .overflow-hidden {    overflow: hidden  }  .p-story {    padding: var(--spacing-story)  }  .py-xs {    padding-block: var(--spacing-xs)  }  .pt-sm {    padding-top: var(--spacing-sm)  }  .\(cursor\:pointer\) {    cursor: pointer  }}
切換配色,重點色和表面色會一起改變,文章間距維持相同。打開短文可繼續閱讀。

fg-brand 讀取 --color-brand,p-story 讀取 --spacing-story。改一個 token,所有使用它的元素就會一起更新。下方兩張卡片的重點色隨配色切換,內距都維持 1.5rem。

共用 token,切換兩種配色

兩張卡片共用同一份標記。原生 light-dark() 選出重點色,量尺顯示 p-story 在兩張卡片上都提供 1.5rem 的內容內距。

文字說明
  1. 樣式表把 --color-brand 設為 light-dark(#665442,
  2. 兩張文字卡片都使用 fg-brand 和 p-story。亮色卡片用第一個重點色,暗色卡片用第二個。文字、版面和 1.5rem 內距都相同。

原生 light-dark() 會依目前的 color-scheme 選用重點色。預設色盤中按用途命名的表面色和文字色,也會跟著配色切換。把需要一起變動的設計決策命名為 token,避免只因為目前的值相同就將它們綁在一起。

#為響應式條件命名

用 @custom-media 為檢視區條件命名,或用 @custom-container 為容器條件命名。

@custom-media --reading (width >= 60rem);@custom-container --story-wide (width >=28rem);
<section class="container-inline (container-name:story)">  <article class="grid gap-md p-md p-lg@reading grid-cols(2)@container(story_--story-wide)">    <h2 class="m(0)">A layout that knows its space</h2>    <p class="m(0)">Two columns when this container has room.</p>  </article></section>
產生的 CSS
@layer theme {  :root,  :host {    --spacing-md: 1rem;    --spacing-lg: 1.5rem  }}@layer utilities {  .container-inline {    container-type: inline-size  }  .grid {    display: grid  }  .gap-md {    gap: var(--spacing-md)  }  .m\(0\) {    margin: calc(0 * 1px)  }  .p-md {    padding: var(--spacing-md)  }  .\(container-name\:story\) {    container-name: story  }  @container story (width >= 28rem) {    .grid-cols\(2\)\@container\(story_--story-wide\) {      display: grid;      grid-template-columns: repeat(2, minmax(0, 1fr))    }  }  @media (width >= 60rem) {    .p-lg\@reading {      padding: var(--spacing-lg)    }  }}

p-lg@reading 依檢視區寬度而定,grid-cols(2)@container(story_--story-wide) 則看父層中名為 story 的容器。前者回應頁面寬度,後者回應元件可用的空間。在 class 中,_ 代表值或查詢裡的空格,因此 @container(story_--story-wide) 會以 --story-wide 判斷 story 容器;字面上的底線則寫成 \_。在應用的樣式表中,同樣的條件也可寫成 @media (--reading) 和 @container story (--story-wide)。Master CSS 會把兩種寫法都展開為一般 CSS 查詢。

從主題 token和色彩了解如何建立符合專案需求的設計基礎。

#用原生 CSS 建立可重用的樣式

文章連結、導覽項目或對話框都可以有自己的 CSS。preset 建立五個串接層,分別是 theme、base、defaults、components 和 utilities。把共用樣式放進 components,再用 utility 做局部調整。

@layer components {  .story-link {    display: inline-flex;    padding-inline: 1rem;    padding-block: 0.5rem;    border-radius: 0.375rem;    background-color: var(--color-surface-raised);    color: var(--color-text-body);    text-decoration: none;  }}
<a class="story-link" href="/journal">Read the journal</a><a class="story-link px-lg" href="/journal">Read the journal</a>
產生的 CSS
@layer components {  .story-link {    background-color: var(--color-surface-raised);    color: var(--color-text-body);    border-radius: .375rem;    padding-block: .5rem;    padding-inline: 1rem;    text-decoration: none;    display: inline-flex;  }}@layer theme {  :root,  :host {    --color-zinc-30: oklch(78% .0025 265);    --color-zinc-70: oklch(39.8% .0028 265);    --color-zinc-90: oklch(24.6% .0022 265)  }  :root,  :host {    --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90));    --color-text-body: light-dark(var(--color-zinc-70), var(--color-zinc-30))  }  :root,  :host {    --color-white: oklch(100% 0 none)  }  :root,  :host {    --spacing-lg: 1.5rem  }}@layer utilities {  .px-lg {    padding-inline: var(--spacing-lg)  }}

兩個連結都使用 .story-link。第二個加上 px-lg,將水平內距從 1rem 增加到 1.5rem。對普通宣告而言,排在後面的 utilities 層優先於 components 層。

共用元件,局部調整

在這些層的普通宣告中,utilities 的優先順序高於 components。藍色區塊標示內距的位置。

文字說明
  1. components 層的 .story-link 元件設有 1rem 的水平內距,兩個連結共用它的形狀與字體樣式。
  2. 第二個連結加上 utilities 層的 px-lg,兩側內距各變成 1.5rem。連結文字維持相同。

#選擇要重用的內容

依要共用的內容選擇工具。

要共用的內容寫法
設計值在 @theme 定義 token。
元件樣式在 @layer components 定義原生 class。
按需生成的 class使用 @utility 定義。
宣告和條件定義 @mixin,再用 @apply 展開。

mixin 本身不會建立 class。下方範例的 --action-row 為原生元件和按需生成的 utility 提供相同的宣告。

@mixin --action-row {  display: inline-flex;  align-items: center;  gap: var(--spacing-xs);}@utility action-row {  @apply --action-row;}@mixin --hocus {  &:is(:hover, :focus-visible) {    @contents;  }}@layer components {  .story-action {    @apply --action-row;  }}
<a class="story-action underline@apply(--hocus)" href="/journal">Read a story</a><a class="action-row underline@apply(--hocus)" href="/archive">Browse the archive</a>
產生的 CSS
@layer components {  .story-action {    display: inline-flex;    align-items: center;    gap: var(--spacing-xs)  }}@layer theme {  :root,  :host {    --spacing-xs: .5rem  }}@layer utilities {  .action-row {    display: inline-flex;    align-items: center;    gap: var(--spacing-xs)  }  .underline\@apply\(--hocus\):is(:hover, :focus-visible) {    text-decoration-line: underline  }}

--hocus 共用滑鼠停留與鍵盤焦點的選擇器。underline@apply(--hocus) 將底線樣式放進它的 @contents 插槽。@apply 接受的是 mixin 呼叫,不能傳入 utility 清單。 Master CSS 會編譯它支援的靜態 mixin 語法,瀏覽器不必原生支援 CSS mixin。

自訂 utility 也可接受靜態參數,或將 token 命名空間連到一組 class。專案自己的詞彙會與 preset 一起提供給編譯器和工具。

重用樣式說明這些寫法,串接層則介紹包括重要宣告與未分層樣式在內的優先順序。

#選擇如何交付 CSS

依照應用程式何時能取得完整的 class 名稱,選擇適合的渲染模式。

預設使用靜態模式。 在建置時掃描完整的 class 名稱並輸出樣式表。瀏覽器無須載入 Master CSS runtime。

預先渲染模式(prerender)將生成的 CSS 隨 HTML 一起交付。 當整合能讀取完成的 HTML,且其中已包含頁面需要的所有 class 時,就適合使用這種模式。瀏覽器無須載入 Master CSS runtime。

Runtime 模式在瀏覽器生成 CSS。 編輯器、線上實驗環境或部署後更新的內容若會引入建置時未知的 class,可以使用這種模式。Runtime 和漸進式模式都包含瀏覽器端 JavaScript。

漸進式模式從已渲染的 HTML 開始。 先交付頁面的初始 CSS,等後續出現尚未涵蓋的 class,再由瀏覽器生成規則。只要已有有效的初始 CSS 和 hydration manifest,預設 runtime 就可以等到那時再載入引擎。

同一套寫法,四種 CSS 交付方式

每條路徑都可獨立使用。漸進式模式在有有效 hydration 資料時,可延後載入引擎。

文字說明
  1. 靜態模式在建置時掃描來源中的完整 class,瀏覽器收到樣式表,無須載入 Master CSS runtime。
  2. 預先渲染模式根據已渲染的 HTML 生成初始 CSS,並隨該 HTML 交付。無須載入 Master CSS runtime。
  3. Runtime 模式依據已連接 DOM 的 class 在瀏覽器生成 CSS,可以處理稍後才出現的 class,並會載入瀏覽器端 JavaScript。
  4. 漸進式模式從渲染後的 HTML 取得初始 CSS。有效的樣式表與頁面 manifest 讓預設 runtime 可以等到出現未涵蓋的 class 再載入引擎。

Fieldwork 使用靜態模式,也能展開文章、回應媒體查詢與切換主題。介面可以互動,CSS 仍可在建置時生成。

各種渲染方式的適用範圍

CLI 與 Vite 正式建置會將符合條件的本機匯入合併進生成的 CSS。請部署完整的資源集合,讓保留的樣式表、字型和圖片也能載入。

Next.js 的漸進式整合會處理建置時生成的 HTML。這個模式下,請求時動態生成的 SSR 回應仰賴瀏覽器 runtime。

來源掃描不會執行任意的字串插值。各整合的適用範圍與回應驗證方式,請見渲染模式。

核心語意由 Rust 實作。瀏覽器裡的 JavaScript 引擎會執行編譯後的程式,並與 Rust 的輸出比對,確認結果一致。

控制來源掃描、共用定義與樣式修剪
指令用途
@source / @source not新增或排除要掃描的來源檔案。
@safelist / @blocklist保留或排除候選 class。
@reference共用定義,不帶入被參照檔案的原生 CSS。
@prune native建置提供 class 使用資訊時,允許移除檔案中未使用的原生樣式和受管理的關鍵影格。

預設會保留原生樣式。掃描與修剪說明如何明確設定這些範圍。

#量測頁面實際交付的 CSS

v2.0.0 正式建置中的 Master CSS 指南首頁,行內樣式和連結樣式表合計為 Brotli 壓縮後 24.5 kB,未壓縮 110.4 kB。大部分來自連結的檔案,其中包含文件站自身的元件樣式。該頁生成的 style#master-css 區塊在壓縮前占 833 bytes。

文件頁面使用的 CSS 大小

7 個文件頁面 · 單位 kB

未壓縮
Master CSS
110.4 kB
Angular
151.8 kB
Vue.js
186.3 kB
Material UI
277.4 kB
Bootstrap
293.9 kB
Next.js
479.9 kB
Tailwind CSS
657.7 kB
公開頁面的資料採集於 2026年11月4日(UTC),Master CSS 則取自 v2.0.0 的正式建置。加總範圍包括行內樣式與連結的樣式表。Brotli 欄是各份 CSS 分開壓縮後的加總。兩種檢視各用一套刻度。1 kB = 1,000 bytes。 頁面量測快照 · Master CSS v2.0.0 建置清單 · 量測方法與頁面明細

各頁面的設計、內容與建置流程不同。圖表計入初始 HTML 裡的行內樣式與它連結的樣式表,網路傳輸量與框架效能要另外量測。若要用同一個介面做對照,可以看分別用 Master CSS 和 Tailwind CSS 建置同一個介面的結果。

#讀得懂專案的工具

語言服務會讀取專案的定義。你可以在編輯器裡補全 token 名稱、移動游標查看生成的 CSS,以及使用色彩工具。專案自訂的 utility 和 token 也適用。

#一眼看出 class 的組成

VS Code 擴充套件預設使用 active 模式。語意上色會跟著游標所在的 class,讓值、選擇器、查詢、單位和重要宣告標記一目了然,周圍的標記則維持原來的樣式。

看懂一個 class 的組成

<h2 class="m(0) text-lg">Field notes</h2><a class="fg-blue-70:hover@sm" href="/journal">  Read the journal</a>

將游標移到 class 的值上,或按 Tab 移入焦點,即可查看語法色彩。觸控裝置會直接顯示色彩。這個網頁範例使用 Master CSS 的語意分析,VS Code 則跟隨編輯器游標。

各種色彩代表什麼

第一組 class 包含數值函式 m(0) 和 token utility text-lg。第二組結合 fg-blue-70、:hover 選擇器與 @sm 媒體條件。語法上色能辨認這些部分,生成的 CSS 維持相同。

選擇編輯器的語法上色模式

將 masterCSS.embeddedSyntaxHighlighting 設為 active,可替游標所在的 class 上色。always 會為所有偵測到的內嵌 utility 上色,off 則關閉內嵌 utility 的上色。CSS 指令仍會上色,使用中的 VS Code 主題也必須啟用語意上色。

#用程式碼檢查找出問題

ESLint 整合會依照專案設定檢查 class。推薦設定會啟用以下四條規則,規則名稱以 @master/css/ 為前綴。

規則用途
sort-classes維持 class 順序一致並移除重複項目。
no-invalid-classes找出語法錯誤與無效的生成 CSS。
no-conflicting-classes回報相同變體範圍內重疊的宣告。
prefer-canonical-classes建議語意和順序相同的標準寫法。

另有兩條規則可依團隊需求自行啟用。

規則團隊規範
no-unapproved-raw-values對支援 token 的 class 要求使用 token,或明確列出例外。
prefer-component-classes對大量使用組合選擇器的 class 群組,建議抽成元件。

自動修正可調整順序、替換為標準寫法,以及移除完全被覆寫的 class。預設會忽略未知的 class 名稱。是否限制原始值、何時抽成元件,則由作者決定。

從找出衝突到安全修正

依生成 CSS 的順序,p-md 優先。安全修正會移除 p-lg,並保留原本的樣式結果。

文字說明
  1. 對於 <article class="p-md p-lg">Field notes</article>,診斷會建議移除 p-lg,因為生成的 CSS 以 p-md 覆寫它。
  2. 安全修正會產生 <article class="p-md">Field notes</article>。編輯器、lint 工具和 MCP 都會查閱專案解析後的定義。套用預覽時也會檢查來源檔案的雜湊值。

以 p-md p-lg 為例,工具會指出 p-lg 被覆寫,並保留 p-md。CSS 的優先順序不取決於 class 在 HTML 裡的順序。 只有部分重疊的樣式仍需人工確認。Lint 通過後,瀏覽器支援和畫面結果也要另外驗證。設定方式與規則選項請見程式碼檢查。

#讓 AI 讀取專案定義

本機 Master CSS MCP 伺服器讓程式開發代理程式能查詢專案的 token 與 utility、檢查生成的 CSS、執行 lint 並預覽修正。套用預覽時,伺服器會確認受影響的檔案仍與產生預覽時相同。

讓 AI 工具讀取專案的樣式定義

透過本機 stdio 連接 AI 用戶端。伺服器會讀取你設定的專案根目錄。

文字說明
  1. Codex、Claude Code、Cursor 和 Gemini CLI 都是可用的 MCP 用戶端。設定 Master CSS 伺服器時,請指定要檢查的專案根目錄。
  2. Master CSS MCP 使用載入的專案資料提供 manifest 查詢、生成 CSS 檢查、lint 診斷與修正預覽。
  3. 主題 token、utility 和原生 CSS 都留在專案內。套用修正預覽時會檢查來源雜湊值,工具權限由用戶端管理。

連接支援本機 stdio MCP 的用戶端,例如 Codex、Claude Code、Cursor 或 Gemini CLI。設定專案根目錄並確認已載入正確的工作區。工具權限由用戶端管理。

如何在編輯器和代理程式中使用這些專案資訊,請見語言服務與 AI 程式開發流程。

#做出有自己風格的介面

從一個元件開始。加一個狀態,為共用的值命名,再檢查結果。下次遇到實際需要時,就會知道哪些樣式值得重用。

謝謝每一位試用 Master CSS、回報問題或分享作品的人。是你們一起讓 2.0 成形。

安裝 Master CSS,再跟著語法教學做出你的第一個介面。

如果 Master CSS 對你有幫助,歡迎在 GitHub 給它一顆星,讓更多開發者找到它。