ナビゲーション

スタイルの作成

スタイルの再利用

まとめて変える対象に応じてトークン、コンポーネントクラス、ユーティリティ、ミックスインを選び、ローカルな違いも表しやすく保ちます。

このページの内容

二つの画面で保存ボタンを共有しながら、一方だけに広い間隔を与えられます。共通の外観はコンポーネントクラスへ、ローカルな調整はマークアップに残します。小さな単位でも同じです。利用箇所を一緒に変える値には名前を付け、複数のルールに必要な CSS は共有します。

先にインストールと構文チュートリアルを完了してください。各例は自身の CSS とマークアップを含みます。CSS をアプリの既存プリセットインポートの後へ、マークアップをインテグレーションが走査するソースファイルへ追加します:

app.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;    }  }}
生成された 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)  }}
横方向のパディングを比べ、各ボタンにホバーするか Tab で移動してください。操作状態は共通で、二つ目だけが px-lg を使います。スタイルの例であり、データは保存しません。

px-lg の通常のユーティリティ宣言は、プリセットで utilities が components より後にあるため、コンポーネントのパディングに優先します。HTML のクラス順序では決まりません。ローカルな調整は二つ目のボタンに残し、すべてのインスタンスで必要な場合だけ .action に置いてください。競合する宣言はカスケードレイヤーを参照します。

一つの定義、二つの利用箇所

両方のボタンは .action を共有します。ガイドは横方向のパディングを示し、コンポーネントでは片側 1rem、px-lg では 1.5rem です。青いガイドは注釈で、ボタンのスタイルではありません。

テキストによる説明
  1. 共通の定義. components レイヤーのネイティブな .action ルールが、両方のボタンに外観、操作状態、既定のパディングを供給します。
  2. 一つ目のインスタンス. class="action" はコンポーネントの左右各 1rem のパディングを使います。
  3. 二つ目のインスタンス. 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);}
生成された 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 が両方の配置と間隔をそろえ、ほかのクラスが外観を供給します。Add note はスタイルの例で、Choose tokens はテーマガイドを開きます。

完全なクラスで要求するとユーティリティがルールを生成します。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;    }  }}
生成された 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)  }}
どちらのボタンもホバーまたは Tab でラベルに下線が付きます。一方はネイティブ CSS 内で宣言を渡し、もう一方は underline ユーティリティを包みます。フォーカスの輪郭は別途指定しています。

CSS では @apply --hocus の後のブロックが @contents を埋めます。マークアップでは @apply(--hocus) がそのクラスの生成宣言を包みます。ラッパーはトークンを登録せず、隣のクラスを本体に集めることもありません。宣言の組を再利用するならユーティリティかミックスインを使い、共通セレクターや条件の中身を呼び出し側で選ぶときに contents を使います。転送とフォールバックはミックスインの contentsを参照してください。

#呼び出し側が必要とするときにパラメーターを公開する

共通のコントロールサイズは複数のプロパティを連動させる場合があります。別々のプリセットユーティリティでは呼び出し側が計算を繰り返す必要があるなら、パラメーターで関係を表します:

一つのサイズで高さとパディングを制御する

@utility action-size(--height: 2.5rem) {  min-height: var(--height);  padding-inline: calc(var(--height) / 2);}
生成された 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)  }}
action-size() は 2.5rem、action-size(3rem) は 3rem を使います。最小高さと横方向のパディングが引数に従います。ボタンはサイズの例です。

一つの定義、別々の呼び出し値

ルートサイズ 16px では最小高さは 40px と 48px で、padding-inline は選んだ高さの半分です。強調した内側の領域は計測ガイドです。

テキストによる説明

同じ action-size ユーティリティを既定の 2.5rem と 3rem で呼び出す。二つ目は最小高さと横パディングが大きく、それぞれが自身の引数を保つ。

空の呼び出しは宣言した既定値を使います。引数はコンパイル時に分かっている必要があるため、サイズ選択には完全なクラス文字列を使ってください。JavaScript で連続的に変わる値は、ネイティブ CSS のカスタムプロパティに置きます。対応する型と制限はパラメーターの仕様を参照します。

共通定義を変える前に、ローカルな上書きがあるものを含め、少なくとも二つの呼び出し箇所を比べます。ホバーとキーボード状態を確認し、生成 CSS を調べてください。同じ動作が不要になったなら、各インスタンスに例外を足すのではなく定義を分けます。プロジェクト間で安定した語彙を共有するには、パッケージの作成へ進んでください。