自分のデザインを、CSS で表現する。 Master CSS 2.0 では、表現力のあるクラス、プロジェクトのトークン、再利用できるスタイルを、同じ定義を読み取るツールと一緒に使えます。レスポンシブな状態を1つのクラスで指定し、スタイルシートでテーマを定義しながら、生成される CSS を確かめられます。
動画を見る: Master CSS 2.0 — Your project, understood.このページの内容
この方法で作った小さなジャーナル、Fieldwork を見てみましょう。
アイデアをウェブで形にする
HTML と CSS を見る
アプリのスタイルシートで @import '@master/css' の後に 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; } }}<article class="journal r-lg overflow-hidden bg-surface-raised fg-text-body shadow-sm" aria-label="Fieldwork journal"> <header class="journal-head grid gap-md"> <div class="flex flex-wrap items-center justify-between gap-md"> <span class="journal-brand fg-text-strong">Fieldwork<span class="fg-brand">.</span></span> <span class="journal-edition fg-text-muted"><span class="journal-edition-label">An independent journal · </span>Issue 04</span> </div> <nav class="journal-nav fg-text-muted" aria-label="Journal sections"> <a href="#fieldwork-lead">Featured story</a> <a href="#fieldwork-notes">Field notes</a> <a href="#fieldwork-about">About</a> </nav> </header> <div class="journal-grid"> <section id="fieldwork-lead" class="journal-main grid gap-md p-story" aria-labelledby="fieldwork-title"> <img class="journal-photo" src="/images/blog/v2/white-oak-room.webp" alt="White oak chairs and a dining table beside sheer linen curtains" width="1672" height="941" /> <p class="journal-kicker fg-brand">Spaces / Quiet interiors</p> <h2 id="fieldwork-title" class="journal-title fg-text-strong">The everyday, considered.</h2> <p class="journal-deck fg-text-muted">On slowing down, looking closer, and finding a little more in the spaces we inhabit.</p> <p class="journal-meta fg-text-muted">Field note 01 — At home</p> <details class="journal-read"> <summary class="fg-brand">Read the field note</summary> <div class="journal-copy"> <p>Morning light moves across the oak table. A cup, a book, a chair pulled close. The room asks for very little, and gives something back: space to read, to think, to begin again.</p> <p>Its character lives in small decisions. The soft edge of a tabletop. Linen that lets the light through. A place for the objects we reach for every day. Nothing needs to compete for attention.</p> <p>A good home makes room for the everyday.</p> </div> </details> </section> <aside id="fieldwork-notes" class="journal-aside p-story" aria-labelledby="fieldwork-notes-title"> <div class="grid gap-sm pb-lg"> <p class="journal-kicker fg-brand">The art of noticing</p> <h2 id="fieldwork-notes-title" class="journal-aside-title fg-text-strong">Quiet spaces.<br>Everyday rituals.</h2> <p class="journal-deck fg-text-muted">Thoughtful spaces and useful objects. A little less, made with care.</p> </div> <details class="journal-story"> <summary> <span><span class="journal-meta fg-text-muted">02 / Objects</span><span class="journal-story-title block fg-text-strong">Room to make something.</span></span> <img class="journal-story-image" src="/images/blog/v2/ceramic-study.webp" alt="A ceramic pitcher and bowl on a white oak table" width="1536" height="1024" /> </summary> <p>Clay keeps a record of the hand. A slightly uneven rim, a glaze that settles into the surface, a handle shaped to be held. Useful objects can make a daily ritual feel considered.</p> </details> <details class="journal-story"> <summary> <span><span class="journal-meta fg-text-muted">03 / In practice</span><span class="journal-story-title block fg-text-strong">The things we choose to keep.</span></span> <span class="journal-story-arrow fg-text-muted" aria-hidden="true">+</span> </summary> <p>A room takes shape through what stays. Start with the pieces you use, give each one a place, and leave a little space between them. The goal is a home that works for the life inside it.</p> </details> </aside> </div> <footer id="fieldwork-about" class="journal-about fg-text-muted"> <p>Fieldwork is an illustrative journal about thoughtful spaces and everyday rituals, created for the Master CSS 2.0 release.</p> <span>Good spaces. Considered living.</span> </footer></article>生成された 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) }}プレビューの幅を狭めると、サイドバーが本文と同じ列に並びます。記事を開けば、内容に合わせてレイアウトが広がります。テーマを切り替えるとアクセント色が変わり、共通の余白がレイアウトのリズムを保ちます。HTML と CSS はプレビュー内で確認できます。
#1つのクラスで、値と条件を表す
Master CSS では、スタイルの指定を対象の要素に直接書けます。レイアウトには flex や grid、プロジェクトのトークンを使うには gap-md や p-lg を書きます。セレクターや条件は、対象のクラスに付け足します。
<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 の色トークンを使う、という意味です。プリセットでのしきい値は 52.125rem です。Master CSS がルールを生成し、ブラウザーがセレクターとメディアクエリーを評価します。
1つのクラスで条件を指定する
値、セレクター、条件を1つのクラスにまとめられます。ブラウザーは、生成されたセレクターとメディアクエリーを評価します。
テキストによる説明
- fg-blue-70 は color: var(--color-blue-70) を指定します。:hover はクラスが付いた要素のホバー状態を指定します。
- @sm はプリセットのメディア条件 (width >= 52.125rem) を使います。クエリーとホバー状態の両方が一致すると適用されます。
指定したい内容に合わせて書き方を選べます。
| クラス | 意味 |
|---|---|
p-lg | --spacing-lg トークンを使うパディング。 |
m(0) | マージンを 0 にする。 |
p(16) | 16px のパディング。 |
grow(1) | フレックス項目の伸長係数を単位なしの 1 にする。 |
grid-cols(3) | 等幅の3列グリッド。 |
(aspect-ratio:4/3) | ネイティブ CSS のプロパティと値。 |
数値を取る関数では、プロパティによって値の解釈が変わります。p(16) は長さを、grow(1) は単位なしの数値を表します。ネイティブ CSS の宣言には、CSS 関数やカスタムプロパティも使えます。(width:var(--progress)) なら、クラスを変えずに JavaScript から値を更新できます。
クラスの組み合わせ方をさらに見る
| クラス | 意味 |
|---|---|
fg-blue-60/.5 · -mt-sm | 不透明度 50% の色、または間隔トークンの負の値。 |
fg-blue-60:hover>a · fg-blue-60_a | 親がホバーされたときの直下のリンク、または子孫のリンク。 |
grid@supports(display:grid) | ネイティブの機能クエリーでルールを囲む。 |
(opacity:0)@starting-style | CSS トランジション開始時の不透明度。 |
p-md! | 重要指定を付けたパディングの宣言。 |
:focus-visible、:has()、::before などのセレクターは、@media(...)、@supports(...)、@container(...) と組み合わせられます。条件を複数指定すると入れ子になり、すべてが一致したときに適用されます。生成結果は通常の CSS として確認できます。
実際に書いてみるなら、構文チュートリアルから始められます。
#プロジェクト独自のデザインシステム
まずはプリセットの色、間隔、タイポグラフィ、角丸、影、モーションのトークンを使います。インターフェースに独自のスタイルが見えてきたら、@theme でその選択に名前を付けます。
#広色域のカラーパレット
17色相にそれぞれ11段階の色があります。プリセットは OKLCH を使い、sRGB を超えて Display P3 の色域に入る色も含みます。各列は明るい色から暗い色へ並び、表示できる色の範囲はディスプレイによって異なります。
fg-blue-50 や bg-green-90 で色階を直接指定することも、役割を表す名前を定義することもできます。Fieldwork では、アクセント色と記事の内側の余白を共通化しています。アプリのスタイルシートで、@import '@master/css' の後に次の定義を追加します。
自分の配色で記事を表示する
@theme { --color-brand: light-dark(#665442, #d7c2a6); --spacing-story: 1.5rem;}<article class="max-w-sm mx-auto overflow-hidden r-lg bg-surface-raised shadow-sm"> <div class="grid gap-sm p-story"> <p class="m(0) text-xs fg-brand">Fieldwork / Issue 04</p> <h2 class="m(0) text-2xl font-regular fg-text-strong">The everyday, considered.</h2> <p class="m(0) text-sm fg-text-muted">A field note on slowing down, looking closer, and finding a little more in the spaces we inhabit.</p> <details class="text-sm"> <summary class="(cursor:pointer) fg-brand py-xs">Read the field note</summary> <p class="m(0) pt-sm fg-text-body">Morning light moves across the oak table. A cup, a book, a chair pulled close. The room asks for very little, and gives something back: space to read, to think, to begin again. A good home makes room for the everyday.</p> </details> </div></article>生成された 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 を参照します。トークンを変えると、それを使うすべての要素に変更が反映されます。下の例では、配色に応じてアクセント色が変わり、どちらのカードも同じ 1.5rem の内側の余白を使います。
共通のトークンで2つのカラースキーム
どちらのカードも同じマークアップです。CSS 標準の light-dark() がアクセント色を選び、計測ガイドは p-story による 1.5rem の内側の余白を示しています。
テキストによる説明
- スタイルシートでは --color-brand を light-dark(#665442, #d7c2a6)、--spacing-story を 1.5rem と定義します。
- 2つのテキストパネルは fg-brand と p-story を使います。ライトのパネルは最初のアクセント色を、ダークのパネルは2番目の色を使います。テキスト、レイアウト、1.5rem のパディングは共通です。
CSS 標準の light-dark() が、適用中の color-scheme に応じてアクセント色を選びます。プリセットで面や文字の役割を表す色も、同じ配色に従います。たまたま値が同じというだけでまとめず、一緒に変えるべきスタイルに名前を付けましょう。
#レスポンシブ条件に名前を付ける
ビューポートの条件には @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 の幅に応じます。一方はページ全体の表示幅に、もう一方はコンポーネントが使える空間に合わせて変化します。クラスでは _ が値やクエリー内のスペースを表すため、@container(story_--story-wide) は story コンテナーを --story-wide で判定します。アンダースコアそのものは \_ と書きます。アプリのスタイルシートでも、同じ条件を @media (--reading) と @container story (--story-wide) で指定できます。Master CSS はどちらの書き方も通常の CSS クエリーに展開します。
プロジェクトに合わせてスタイルの土台を整える方法は、テーマトークンと色を参照してください。
#ネイティブ CSS でスタイルを再利用する
記事へのリンク、ナビゲーション項目、ダイアログには、それぞれ独自の CSS を定義できます。プリセットには theme、base、defaults、components、utilities の5つのカスケードレイヤーがあります。共通のスタイルは components に置き、個別の調整にはユーティリティを使います。
@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 を使います。2つ目に px-lg を追加すると、左右のパディングが 1rem から 1.5rem に広がります。通常の宣言では、後にある utilities レイヤーが components より優先されます。
コンポーネントを共有し、片方だけを調整する
これらのレイヤーの通常の宣言では、utilities が components より優先されます。青い帯はパディングを確認するための計測ガイドです。
テキストによる説明
- components レイヤーの .story-link が、左右それぞれ 1rem のパディングを定義します。リンクの形状とタイポグラフィも共通です。
- 2つ目のリンクは utilities レイヤーの px-lg を追加します。リンクの文言はそのままで、左右のパディングがそれぞれ 1.5rem になります。
#何を再利用するかで書き方を選ぶ
共有したい内容によって、適した書き方が異なります。
| 共有するもの | 書き方 |
|---|---|
| デザインで使う値 | @theme のトークン。 |
| コンポーネントのスタイル | @layer components に置くネイティブのクラス。 |
| 必要に応じて生成するクラス | @utility の定義。 |
| 宣言や条件 | @apply で展開する @mixin の定義。 |
ミックスイン自体はクラスを作りません。この例では、--action-row が、ネイティブのコンポーネントと必要に応じて生成されるユーティリティに、同じ宣言を提供します。
@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 に渡すのはミックスインの呼び出しです。ユーティリティクラスの一覧は渡せません。 Master CSS が対応する静的ミックスイン構文をコンパイルするので、ブラウザーのネイティブ CSS ミックスイン対応は不要です。
カスタムユーティリティでは、静的なパラメーターを受け取ったり、トークンの名前空間をクラスの系統に結び付けたりすることもできます。コンパイラーとツールは、プリセットと同じようにプロジェクト独自の定義を認識します。
これらの使い分けはスタイルの再利用、重要宣言やレイヤーに属さないスタイルの優先順位はカスケードレイヤーを参照してください。
#CSS の配信方法を選ぶ
アプリがクラス名をいつ把握できるかで、レンダリングモードを選びます。
既定は静的レンダリングです。 ビルド時に完全なクラス名をスキャンし、スタイルシートを配信します。ブラウザーに Master CSS ランタイムは不要です。
事前レンダリングモード(prerender)は、生成した CSS を HTML とともに配信します。 インテグレーションがレンダリング済みの HTML を読み取れ、その HTML にページで使うクラスがすべて含まれている場合に使います。ブラウザーに Master CSS ランタイムは不要です。
ランタイムレンダリングは、ブラウザーで CSS を生成します。 エディター、プレイグラウンド、公開後に配信されるコンテンツなどで、ビルド時には未知のクラス名が現れる場合に使います。ランタイムモードとプログレッシブモードは、どちらもブラウザーで JavaScript を使います。
プログレッシブレンダリングは、レンダリング済みの HTML から始まります。 初期 CSS を配信し、その後に現れた未対応のクラスのルールをブラウザーで生成します。有効な事前レンダリング済み CSS とハイドレーションマニフェストがあれば、既定のランタイムはその時点までエンジンの読み込みを遅らせられます。
同じ書き方で、4つの配信方法
各経路は独立した選択肢です。プログレッシブモードでは、ハイドレーションデータが有効ならエンジンの読み込みを遅らせられます。
テキストによる説明
- 静的レンダリングでは、ビルド時にソースの完全なクラス名をスキャンします。ブラウザーは CSS スタイルシートを受け取り、Master CSS ランタイムは不要です。
- 事前レンダリングモードでは、レンダリング済みの HTML から初期 CSS を生成して一緒に配信します。Master CSS ランタイムは不要です。
- ランタイムレンダリングでは、DOM に接続された要素のクラスから、ブラウザーで CSS を生成します。後から追加されたクラス名に対応し、ブラウザーで JavaScript を使います。
- プログレッシブレンダリングでは、レンダリング済み HTML から初期 CSS を用意します。有効なスタイルシートとページマニフェストがあれば、既定のランタイムは未対応のクラスが現れた時点でエンジンを読み込みます。
Fieldwork は静的レンダリングでも記事を展開でき、メディアクエリーに応じた表示やテーマ切り替えにも対応できます。操作に応じて表示が変わっても、CSS をランタイムで生成する必要はありません。
レンダリングと配信で確認すること
CLI と Vite の本番ビルドは、条件を満たすローカルインポートを生成 CSS にまとめます。別ファイルとして残るスタイルシート、フォント、画像も利用できるように、必要なアセット一式をデプロイしてください。
Next.js のプログレッシブアダプターは、ビルド時に生成された HTML を処理します。このモードで、リクエスト時の動的 SSR レスポンスに対する CSS の生成は、ブラウザーのランタイムに任されます。
ソーススキャンは任意の文字列補間を評価しません。アダプターが処理する範囲や、レスポンスの確認方法はレンダリングモードを参照してください。
コアの動作は Rust が担います。ブラウザーでは JavaScript エンジンがコンパイル済みのプログラムを実行し、その出力が Rust 版と一致することを検証しています。
スキャン、定義の共有、未使用スタイルの削除を制御する
| ディレクティブ | 用途 |
|---|---|
@source / @source not | スキャンするソースファイルを追加または除外する。 |
@safelist / @blocklist | クラスの候補を保持または除外する。 |
@reference | 参照先のネイティブ CSS をインポートせず、定義を共有する。 |
@prune native | ビルドがクラスの使用状況を把握できる場合に、そのファイルのネイティブスタイルと管理対象のキーフレームを削除の対象にする。 |
既定ではネイティブスタイルを保持します。各設定の適用範囲を明示する方法は、スキャンと未使用スタイルの削除を参照してください。
#配信する CSS のサイズを測る
v2.0.0 の本番ビルドでは、Master CSS ガイドのインラインスタイルとリンクされたスタイルシートを合わせると、Brotli 圧縮で 24.5 kB、圧縮前は 110.4 kB です。その大半は、サイト固有のコンポーネントのスタイルも含む外部スタイルシートが占めます。このページの生成 CSS を含む style#master-css ブロックは、圧縮前で 833 バイトです。
ドキュメントページが配信する CSS
ドキュメント 7 ページの CSS(kB)
ページごとにデザイン、コンテンツ、ビルド工程が異なります。グラフの集計対象は、初期 HTML に含まれるインラインスタイルと、そこからリンクされるスタイルシートです。ネットワークの転送量やフレームワークの性能は測定していません。条件を揃えた比較は、同じ UI を Master CSS と Tailwind CSS で作るベンチマークを参照してください。
#プロジェクトの定義を理解するツール
言語サービスはプロジェクトの定義を読み取ります。エディターでトークン名を補完し、ホバーで生成 CSS を確認し、色を編集できます。プロジェクト独自のユーティリティやトークンも同じように扱えます。
#クラスの構造を一目で読む
VS Code 拡張機能の既定の active モードでは、カーソル位置のクラスにセマンティックハイライトが付きます。周囲のマークアップを強調せず、値、セレクター、クエリー、単位、重要指定のマーカーを見分けられます。
クラスの構成を読み解く
<h2 class="m(0) text-lg">Field notes</h2><a class="fg-blue-70:hover@sm" href="/journal"> Read the journal</a>クラス属性の値にマウスを重ねるか、Tab キーでフォーカスすると構文の色分けが表示されます。タッチ端末では最初から色分けされています。このウェブデモは Master CSS のセマンティック解析を使い、VS Code ではエディターのカーソル位置に連動します。
色分けが示すもの
1つ目のクラス一覧には、数値を取る関数 m(0) とトークンユーティリティ text-lg があります。2つ目は fg-blue-70、セレクター :hover、メディア条件 @sm を組み合わせています。生成 CSS を変えずに、各部分を色で見分けられます。
エディターのハイライトモードを選ぶ
masterCSS.embeddedSyntaxHighlighting に active を設定するとカーソル位置のクラスを、always では検出されたすべての埋め込みユーティリティをハイライトします。off では埋め込みユーティリティのハイライトが無効になります。CSS ディレクティブの構文は引き続きハイライトされます。使用中の VS Code テーマでセマンティックハイライトが有効になっている必要があります。
#lint で記述の誤りを見つける
ESLint 連携では、プロジェクトの定義に照らしてクラスを検証できます。推奨設定では、次の4つのルールが有効になります。ルール名には @master/css/ が付きます。
| ルール | 用途 |
|---|---|
sort-classes | クラスの順序を揃え、重複を取り除く。 |
no-invalid-classes | 不正な構文や、無効な CSS を生成するクラスを検出する。 |
no-conflicting-classes | 同じバリアントのスコープで重複する宣言を報告する。 |
prefer-canonical-classes | 意味と順序を保てる場合に、標準的な書き方を勧める。 |
必要に応じて、次の2つのルールも明示的に有効にできます。
| ルール | チームの方針 |
|---|---|
no-unapproved-raw-values | トークンに対応するクラスに対して、トークンを使うか、直接指定した値を例外として明示するよう求める。 |
prefer-component-classes | 結合子を使うクラスが多い場合に、コンポーネントへの切り出しを提案する。 |
自動修正は、並べ替え、標準的な書き方への置き換え、完全に上書きされるクラスの除去に対応します。認識できないクラス名は既定で無視されます。直接指定する値やコンポーネントに関する方針は、書き手が判断します。
競合を見つけて安全に修正する
生成 CSS では p-md が優先されます。p-lg を取り除けば、適用結果を保ったまま安全に修正できます。
テキストによる説明
- <article class="p-md p-lg">Field notes</article> では、生成 CSS で p-md が p-lg を上書きするため、競合診断は p-lg の削除を勧めます。
- 修正結果は <article class="p-md">Field notes</article> です。エディター、lint ツール、MCP はプロジェクトで解決された定義を参照します。プレビューを適用する際は、ソースファイルのハッシュも確認します。
p-md p-lg では、ツールは p-lg が上書きされると報告し、p-md を残します。CSS の優先順位は、HTML に書いたクラスの順序では決まりません。 宣言が部分的に重複する場合は確認が必要です。lint を通過しても、ブラウザーでの対応や意図した見た目までは保証されません。設定手順とルールの選択肢は、コードの lintを参照してください。
#AI とプロジェクトの定義を共有する
ローカルの Master CSS MCP サーバーを使うと、コーディングエージェントも同じプロジェクトの定義を参照できます。トークンやユーティリティの検索、生成 CSS の確認、lint の実行、修正のプレビューが可能です。プレビューを適用する際は、対象ファイルがプレビュー作成時から変わっていないかを確認します。
プロジェクトの定義を AI ツールで活用する
AI クライアントはローカルの stdio で接続します。サーバーは指定したプロジェクトルートを読み込みます。
テキストによる説明
- Codex、Claude Code、Cursor、Gemini CLI は、いずれも MCP クライアントとして使えます。それぞれのクライアントで調べるプロジェクトのルートを、Master CSS MCP サーバーに指定します。
- Master CSS MCP では、読み込んだプロジェクトのマニフェストの検索、生成 CSS の調査、lint 診断、修正プレビューが可能です。
- テーマトークン、ユーティリティ、ネイティブ CSS はプロジェクトに置いたまま使えます。修正プレビューの適用時にはソースのハッシュを確認します。ツールの権限はクライアントが管理します。
Codex、Claude Code、Cursor、Gemini CLIなど、ローカルの stdio MCP に対応するクライアントで接続します。プロジェクトルートを指定し、意図したワークスペースが読み込まれていることを確認してください。ツールの権限はクライアントが管理します。
エディターやエージェントでプロジェクトの定義を活用する方法は、言語サービスとAI コーディングの手順を参照してください。
#自分らしいインターフェースを作る
まずは1つのコンポーネントから。状態を追加し、共有する値に名前を付け、結果を確かめます。何を再利用するかは、次に必要になったときに考えてみてください。
Master CSS を試し、問題を報告し、作ったものを共有してくださった皆さん、ありがとうございます。皆さんの協力が 2.0 を形作りました。
Master CSS をインストールして、構文チュートリアルで最初のインターフェースを作ってみましょう。
Master CSS がお役に立っているなら、GitHub でスターを付けて、ほかの開発者が見つけやすくなるようご協力ください。












