スタイルの再利用
まとめて変える対象に応じてトークン、コンポーネントクラス、ユーティリティ、ミックスインを選び、ローカルな違いも表しやすく保ちます。
このページの内容
二つの画面で保存ボタンを共有しながら、一方だけに広い間隔を与えられます。共通の外観はコンポーネントクラスへ、ローカルな調整はマークアップに残します。小さな単位でも同じです。利用箇所を一緒に変える値には名前を付け、複数のルールに必要な CSS は共有します。
先にインストールと構文チュートリアルを完了してください。各例は自身の CSS とマークアップを含みます。CSS をアプリの既存プリセットインポートの後へ、マークアップをインテグレーションが走査するソースファイルへ追加します:
@import '@master/css';#コンポーネントの共通の外観をまとめる
アクションボタンのような製品上の役割には、どこで使っても認識できる外観が必要です。@layer components 内のネイティブクラスとして定義し、すべてのインスタンスに属する状態も含めてください。
同じボタンの間隔をローカルに調整する
@theme { --spacing-action-x: 1rem; --color-action: var(--color-surface-raised);}@layer components { .action { display: inline-flex; align-items: center; justify-content: center; min-height: 2.375rem; padding: .5rem var(--spacing-action-x); border: 1px solid var(--color-line-divider); border-radius: var(--radius-md); background-color: var(--color-action); color: var(--color-text-strong); box-shadow: var(--shadow-sm); font: inherit; font-size: var(--font-size-sm); font-weight: 400; line-height: 1.2; &:is(:hover, :focus-visible) { box-shadow: var(--shadow-md); } &:active { background-color: var(--color-surface-inset); box-shadow: none; } &:focus-visible { outline: 2px solid var(--color-text-strong); outline-offset: 3px; } }}<div class="flex flex-wrap items-start gap-lg"> <div class="flex flex-col items-start gap-sm"> <p class="m(0) text-xs fg-text-muted">Shared component</p> <button type="button" class="action">Save changes</button> </div> <div class="flex flex-col items-start gap-sm"> <p class="m(0) text-xs fg-text-muted">Local adjustment</p> <button type="button" class="action px-lg">Save changes</button> </div></div>生成された CSS
@layer components { .action { min-height: 2.375rem; padding: .5rem var(--spacing-action-x); border: 1px solid var(--color-line-divider); border-radius: var(--radius-md); background-color: var(--color-action); color: var(--color-text-strong); box-shadow: var(--shadow-sm); font: inherit; font-size: var(--font-size-sm); justify-content: center; align-items: center; font-weight: 400; line-height: 1.2; display: inline-flex; &:is(:hover, :focus-visible) { box-shadow: var(--shadow-md); } &:active { background-color: var(--color-surface-inset); box-shadow: none; } &:focus-visible { outline: 2px solid var(--color-text-strong); outline-offset: 3px; } }}@layer theme { :root, :host { --color-zinc-5: oklch(96.5% .0011 265); --color-zinc-40: oklch(68.6% .0028 265); --color-zinc-60: oklch(48.6% .003 265); --color-zinc-90: oklch(24.6% .0022 265); --color-zinc-100: oklch(15% .0016 265) } :root, :host { --color-surface-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100)); --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)); --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --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-size-xs: .75rem; --font-size-sm: .875rem; --text-xs: var(--font-size-xs); --text-xs--line-height: 1.5; --text-xs--letter-spacing: 0; --radius-md: .375rem; --spacing-sm: .75rem; --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)); --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 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)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26)) } :root, :host { --spacing-action-x: 1rem; --color-action: var(--color-surface-raised) }}@layer utilities { .text-xs { font-size: var(--text-xs); line-height: var(--text-xs--line-height, normal); letter-spacing: var(--text-xs--letter-spacing, normal) } .items-start { align-items: start } .fg-text-muted { color: var(--color-text-muted) } .flex { display: flex } .flex-col { flex-direction: column } .flex-wrap { flex-wrap: wrap } .gap-lg { gap: var(--spacing-lg) } .gap-sm { gap: var(--spacing-sm) } .m\(0\) { margin: calc(0 * 1px) } .px-lg { padding-inline: var(--spacing-lg) }}px-lg の通常のユーティリティ宣言は、プリセットで utilities が components より後にあるため、コンポーネントのパディングに優先します。HTML のクラス順序では決まりません。ローカルな調整は二つ目のボタンに残し、すべてのインスタンスで必要な場合だけ .action に置いてください。競合する宣言はカスケードレイヤーを参照します。
一つの定義、二つの利用箇所
両方のボタンは .action を共有します。ガイドは横方向のパディングを示し、コンポーネントでは片側 1rem、px-lg では 1.5rem です。青いガイドは注釈で、ボタンのスタイルではありません。
テキストによる説明
- 共通の定義. components レイヤーのネイティブな .action ルールが、両方のボタンに外観、操作状態、既定のパディングを供給します。
- 一つ目のインスタンス. class="action" はコンポーネントの左右各 1rem のパディングを使います。
- 二つ目のインスタンス. class="action px-lg" は左右各 1.5rem のパディングを要求します。utilities レイヤーは components の後なので、px-lg はこのインスタンスの横パディングを上書きし、ほかのコンポーネントスタイルは残ります。図の青い帯と物差しはパディングを示す計測注釈で、ユーティリティが追加するスタイルではありません。
ネイティブのコンポーネントルールは、既定でスタイルシートとともに出力されます。ネイティブの枝刈りは明示的に有効化します。コンポーネントの状態は CSS ルール内に書いてください。ネイティブの .action 定義は action:hover ユーティリティを登録しません。
外観の共有と配信の共有は別の判断です。再利用コンポーネントのルールが無関係なページにも読み込まれる場合は、CSS 配信の最適化を参照してください。
#一緒に変えるものを選ぶ
繰り返すクラス一覧を抽出する前に、共通の判断を特定してください。似た見た目の要素が、常に一緒に変わる必要はありません。
| 共有するもの | 使うもの |
|---|---|
| スタイル間の値 | テーマトークン |
| 製品上の役割と状態 | ネイティブのコンポーネントクラス |
| マークアップで組み合わせる機能 | ユーティリティ |
| CSS 内の宣言やラッパー | ミックスイン |
最初の例は、操作の色とパディングにそれぞれトークンを与えます。ほかのコンポーネントは、ボタンの背景、形、状態を引き受けずに、CSS の var() やマークアップの px-action-x で --spacing-action-x を読めます。両方を変える場合はトークンを変更し、値が偶然同じで目的が違う場合は別のトークンにしてください。命名、別名、スコープ付きの上書きはテーマトークンで説明します。
繰り返し使うコンポーネントのクラス一覧は、フレームワークのコンポーネント内に置いても構いません。有用な共通スタイルの契約を提供するときに CSS 名を導入し、クラス属性の長さだけを抽出の理由にしないでください。
#スタイルの機能をユーティリティとして登録する
複数のコントロールで、内容を中央にそろえた小さな間隔のインライン行が必要でも、外観と余白は別に保ちたい場合を考えます。共通のレイアウトだけを @utility として登録します:
同じレイアウト、異なるコントロール
@utility action-row { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs);}<div class="flex flex-wrap items-center gap-md"> <button type="button" class="action-row px-md py-xs text-sm r-md b(1) b-solid b-line-divider bg-surface-raised fg-text-strong shadow-sm shadow-md:hover bg-surface-inset:active"> <span aria-hidden="true">+</span>Add note</button> <a href="/guide/theme" target="_top" class="action-row px-sm py-xs text-sm fg-text-body fg-text-strong:hover underline">Choose tokens <span aria-hidden="true">→</span> </a></div>生成された CSS
@layer theme { :root, :host { --color-zinc-5: oklch(96.5% .0011 265); --color-zinc-30: oklch(78% .0025 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-inset: light-dark(var(--color-zinc-5), var(--color-zinc-100)); --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)); --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --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)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-size-sm: .875rem; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --radius-md: .375rem; --spacing-xs: .5rem; --spacing-sm: .75rem; --spacing-md: 1rem } :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)); --shadow-md: 0 0 0 1px light-dark(oklch(0% 0 none / .05), oklch(100% 0 none / .06)), 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)), 0 8px 8px -4px light-dark(oklch(0% 0 none / .045), oklch(0% 0 none / .26)) }}@layer utilities { .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .action-row { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs) } .items-center { align-items: center } .bg-surface-raised { background-color: var(--color-surface-raised) } .b-line-divider { border-color: var(--color-line-divider) } .r-md { border-radius: var(--radius-md) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .shadow-sm { box-shadow: var(--shadow-sm) } .fg-text-body { color: var(--color-text-body) } .fg-text-strong { color: var(--color-text-strong) } .flex { display: flex } .flex-wrap { flex-wrap: wrap } .gap-md { gap: var(--spacing-md) } .py-xs { padding-block: var(--spacing-xs) } .px-md { padding-inline: var(--spacing-md) } .px-sm { padding-inline: var(--spacing-sm) } .underline { text-decoration-line: underline } .shadow-md\:hover:hover { box-shadow: var(--shadow-md) } .fg-text-strong\:hover:hover { color: var(--color-text-strong) } .bg-surface-inset\:active:active { background-color: var(--color-surface-inset) }}完全なクラスで要求するとユーティリティがルールを生成します。action-row@sm のように、プリセットと同じセレクターや条件の接尾辞も使えます。名前はレイアウトの機能を表し、ボタン全体のスタイルを決めるものではありません。
既存のプリセットユーティリティで共通の判断を表せるなら、そのまま使ってください。間隔だけの変更には gap-sm や gap(8) があり、新しいユーティリティは不要です。呼び出し側で一緒に再利用すべき機能を宣言が作る場合に、レシピを定義します。登録と置換はユーティリティの仕様を参照してください。
#ミックスインで CSS を再利用する
ネイティブ CSS のルールで同じ行レイアウトが必要なら、ミックスインを定義し @apply で展開します。次の例では、アクションとツールバーリンクを別々のコンポーネントの役割として保ち、配置を共有します:
@mixin --action-row { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs);}@layer components { .action { @apply --action-row; padding: .5rem 1rem; border: 1px solid var(--color-line-divider); border-radius: var(--radius-md); background-color: var(--color-surface-raised); color: var(--color-text-strong); box-shadow: var(--shadow-sm); font: inherit; font-size: var(--font-size-sm); } .toolbar-link { @apply --action-row; color: var(--color-text-body); font: inherit; font-size: var(--font-size-sm); text-decoration-line: underline; }}<button type="button" class="action"><span aria-hidden="true">+</span> Add note</button><a href="/guide/theme" class="toolbar-link">Choose tokens <span aria-hidden="true">→</span></a>生成された CSS
@layer components { .action { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs); padding: .5rem 1rem; border: 1px solid var(--color-line-divider); border-radius: var(--radius-md); background-color: var(--color-surface-raised); color: var(--color-text-strong); box-shadow: var(--shadow-sm); font: inherit; font-size: var(--font-size-sm) } .toolbar-link { display: inline-flex; align-items: center; justify-content: center; gap: var(--spacing-xs); color: var(--color-text-body); font: inherit; font-size: var(--font-size-sm); text-decoration-line: underline }}@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); --color-zinc-100: oklch(15% .0016 265) } :root, :host { --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)); --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --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)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-size-sm: .875rem; --radius-md: .375rem; --spacing-xs: .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)) }}生成された CSS を開くと、両方のネイティブルールに行の宣言が入り、残りの独自宣言も保持されます。--action-row の定義はマークアップのクラスを作りません。@apply が指定するのはミックスインで、ユーティリティクラスの一覧は受け取りません。
CSS とマークアップの両方に同じ機能が必要なら、宣言元をミックスインに保ち、それを呼ぶユーティリティを加えます:
@utility action-row { @apply --action-row;}この定義をミックスインの例のスタイルシートに追加します。先ほどの独立したユーティリティ本体を置き換え、--action-row の編集がネイティブコンポーネントとユーティリティの両方へ届くようになります。グローバルスタイルで、アプリのエントリーグラフ内の整理方法を参照してください。
クラスを持つユーティリティ、セレクターを埋めるミックスイン
箱は CSS の成果物です。@apply の展開は別のマークアップクラスを登録しません。
テキストによる説明
stack ユーティリティを要求すると display:grid を持つ .stack を出力する。.card 内で --stack ミックスインを適用すると同じ宣言を .card の下へ出力し、セレクターは呼び出し側が持つ。
#contents でラッパーを共有する
繰り返すのが、異なる宣言を囲む条件の場合もあります。ホバーとキーボードフォーカスのミックスインでセレクターを供給し、各呼び出し側が @contents にスタイルを渡せます:
一つの操作ラッパー、二つの呼び出し箇所
@mixin --hocus { &:is(:hover, :focus-visible) { @contents; }}@layer components { .action { @apply --hocus { text-decoration-line: underline; } }}<div class="flex flex-wrap gap-md"> <button type="button" class="action px-md py-xs text-sm r-md b(1) b-solid b-line-divider bg-surface-raised fg-text-strong shadow-sm outline(2):focus-visible outline-solid:focus-visible outline-offset(3)">CSS call site</button> <button type="button" class="px-md py-xs text-sm r-md b(1) b-solid b-line-divider bg-surface-raised fg-text-strong shadow-sm underline@apply(--hocus) outline(2):focus-visible outline-solid:focus-visible outline-offset(3)">Class call site</button></div>生成された CSS
@layer components { .action:is(:hover, :focus-visible) { text-decoration-line: underline }}@layer theme { :root, :host { --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-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-size-sm: .875rem; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --radius-md: .375rem; --spacing-xs: .5rem; --spacing-md: 1rem } :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)) }}@layer utilities { .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .underline\@apply\(--hocus\):is(:hover, :focus-visible) { text-decoration-line: underline } .bg-surface-raised { background-color: var(--color-surface-raised) } .b-line-divider { border-color: var(--color-line-divider) } .r-md { border-radius: var(--radius-md) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .shadow-sm { box-shadow: var(--shadow-sm) } .fg-text-strong { color: var(--color-text-strong) } .flex { display: flex } .flex-wrap { flex-wrap: wrap } .gap-md { gap: var(--spacing-md) } .outline-offset\(3\) { outline-offset: calc(3 * 1px) } .py-xs { padding-block: var(--spacing-xs) } .px-md { padding-inline: var(--spacing-md) } .outline-solid\:focus-visible:focus-visible { outline-style: solid } .outline\(2\)\:focus-visible:focus-visible { outline-width: calc(2 * 1px) }}CSS では @apply --hocus の後のブロックが @contents を埋めます。マークアップでは @apply(--hocus) がそのクラスの生成宣言を包みます。ラッパーはトークンを登録せず、隣のクラスを本体に集めることもありません。宣言の組を再利用するならユーティリティかミックスインを使い、共通セレクターや条件の中身を呼び出し側で選ぶときに contents を使います。転送とフォールバックはミックスインの contentsを参照してください。
#呼び出し側が必要とするときにパラメーターを公開する
共通のコントロールサイズは複数のプロパティを連動させる場合があります。別々のプリセットユーティリティでは呼び出し側が計算を繰り返す必要があるなら、パラメーターで関係を表します:
一つのサイズで高さとパディングを制御する
@utility action-size(--height: 2.5rem) { min-height: var(--height); padding-inline: calc(var(--height) / 2);}<div class="flex flex-wrap items-center gap-md"> <button type="button" class="action-size() text-sm r-md b(1) b-solid b-line-divider bg-surface-raised fg-text-strong shadow-sm">Default size</button> <button type="button" class="action-size(3rem) text-sm r-md b(1) b-solid b-line-divider bg-surface-raised fg-text-strong shadow-sm">Larger size</button></div>生成された CSS
@layer theme { :root, :host { --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-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-size-sm: .875rem; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --radius-md: .375rem; --spacing-md: 1rem } :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)) }}@layer utilities { .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .action-size\(\) { min-height: 2.5rem; padding-inline: calc(2.5rem / 2) } .action-size\(3rem\) { min-height: 3rem; padding-inline: calc(3rem / 2) } .items-center { align-items: center } .bg-surface-raised { background-color: var(--color-surface-raised) } .b-line-divider { border-color: var(--color-line-divider) } .r-md { border-radius: var(--radius-md) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .shadow-sm { box-shadow: var(--shadow-sm) } .fg-text-strong { color: var(--color-text-strong) } .flex { display: flex } .flex-wrap { flex-wrap: wrap } .gap-md { gap: var(--spacing-md) }}一つの定義、別々の呼び出し値
ルートサイズ 16px では最小高さは 40px と 48px で、padding-inline は選んだ高さの半分です。強調した内側の領域は計測ガイドです。
テキストによる説明
同じ action-size ユーティリティを既定の 2.5rem と 3rem で呼び出す。二つ目は最小高さと横パディングが大きく、それぞれが自身の引数を保つ。
空の呼び出しは宣言した既定値を使います。引数はコンパイル時に分かっている必要があるため、サイズ選択には完全なクラス文字列を使ってください。JavaScript で連続的に変わる値は、ネイティブ CSS のカスタムプロパティに置きます。対応する型と制限はパラメーターの仕様を参照します。
共通定義を変える前に、ローカルな上書きがあるものを含め、少なくとも二つの呼び出し箇所を比べます。ホバーとキーボード状態を確認し、生成 CSS を調べてください。同じ動作が不要になったなら、各インスタンスに例外を足すのではなく定義を分けます。プロジェクト間で安定した語彙を共有するには、パッケージの作成へ進んでください。





