CSS @starting-style応用ガイド


CSS @starting-style応用ガイド

モダンなWebアプリケーションでは、要素の表示・非表示に自然なアニメーションを付けることがUX向上の鍵となります。従来はJavaScriptで要素の状態を管理する必要がありましたが、CSS @starting-styleを使えば、純粋なCSSだけで洗練されたアニメーションを実装できます。

この記事では、@starting-styleの基本から、ダイアログ、ポップオーバー、トースト通知、ドロップダウンメニューなど、実践的なUIパターンの実装方法を詳しく解説します。

@starting-styleとは

@starting-styleは、要素が初めてレンダリングされる際の開始スタイルを定義するCSSルールです。display: noneからdisplay: blockに変化する要素や、DOMに新しく追加された要素に対して、スムーズなトランジションを適用できます。

基本構文

/* 通常の状態 */
.element {
  opacity: 1;
  transform: translateY(0);
  transition: all 0.3s ease;
}

/* 表示開始時の初期状態 */
@starting-style {
  .element {
    opacity: 0;
    transform: translateY(-20px);
  }
}

従来の方法との比較

従来の方法(JavaScript必須)

// 要素を追加
const element = document.createElement('div');
element.classList.add('hidden'); // 初期状態
document.body.appendChild(element);

// 強制リフロー
element.offsetHeight;

// アニメーション開始
element.classList.remove('hidden');

@starting-styleを使った方法

.element {
  opacity: 1;
  transition: opacity 0.3s;
}

@starting-style {
  .element {
    opacity: 0;
  }
}

JavaScriptが不要になり、コードがシンプルで保守しやすくなります。

ダイアログのアニメーション

モーダルダイアログ

<dialog id="modal">
  <div class="modal-content">
    <h2>モーダルタイトル</h2>
    <p>モーダルの内容がここに入ります。</p>
    <button onclick="this.closest('dialog').close()">閉じる</button>
  </div>
</dialog>

<button onclick="document.getElementById('modal').showModal()">
  モーダルを開く
</button>
dialog {
  border: none;
  border-radius: 12px;
  padding: 0;
  max-width: 500px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);

  /* アニメーション設定 */
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 0.3s ease,
    transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
    overlay 0.3s allow-discrete,
    display 0.3s allow-discrete;
}

/* 開始時の状態 */
@starting-style {
  dialog[open] {
    opacity: 0;
    transform: scale(0.9);
  }
}

/* 閉じる時の状態 */
dialog:not([open]) {
  opacity: 0;
  transform: scale(0.9);
}

/* バックドロップのアニメーション */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  transition:
    background-color 0.3s,
    overlay 0.3s allow-discrete,
    display 0.3s allow-discrete;
}

@starting-style {
  dialog[open]::backdrop {
    background-color: rgba(0, 0, 0, 0);
  }
}

dialog:not([open])::backdrop {
  background-color: rgba(0, 0, 0, 0);
}

/* モーダルコンテンツ */
.modal-content {
  padding: 2rem;
}

.modal-content h2 {
  margin-top: 0;
}

スライドインダイアログ

/* 下からスライドイン */
dialog.slide-up {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.4s ease,
    transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    overlay 0.4s allow-discrete,
    display 0.4s allow-discrete;
}

@starting-style {
  dialog.slide-up[open] {
    opacity: 0;
    transform: translateY(100%);
  }
}

dialog.slide-up:not([open]) {
  opacity: 0;
  transform: translateY(100%);
}

/* 右からスライドイン */
dialog.slide-left {
  position: fixed;
  right: 0;
  top: 0;
  height: 100vh;
  margin: 0;
  max-width: 400px;

  transform: translateX(0);
  transition:
    transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
    overlay 0.3s allow-discrete,
    display 0.3s allow-discrete;
}

@starting-style {
  dialog.slide-left[open] {
    transform: translateX(100%);
  }
}

dialog.slide-left:not([open]) {
  transform: translateX(100%);
}

ポップオーバーのアニメーション

基本ポップオーバー

<button popovertarget="info-popover">情報を表示</button>

<div id="info-popover" popover>
  <h3>詳細情報</h3>
  <p>ポップオーバーの内容がここに表示されます。</p>
</div>
[popover] {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 1rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  background: white;

  /* アニメーション */
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.2s ease,
    transform 0.2s ease,
    overlay 0.2s allow-discrete,
    display 0.2s allow-discrete;
}

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: translateY(-10px);
  }
}

[popover]:not(:popover-open) {
  opacity: 0;
  transform: translateY(-10px);
}

ツールチップ風ポップオーバー

.tooltip-popover {
  padding: 0.5rem 0.75rem;
  font-size: 0.875rem;
  background: #333;
  color: white;
  border: none;
  border-radius: 4px;

  /* アニメーション */
  opacity: 1;
  transform: translateY(0) scale(1);
  transition:
    opacity 0.15s ease,
    transform 0.15s cubic-bezier(0.16, 1, 0.3, 1),
    overlay 0.15s allow-discrete,
    display 0.15s allow-discrete;
}

@starting-style {
  .tooltip-popover:popover-open {
    opacity: 0;
    transform: translateY(-5px) scale(0.95);
  }
}

.tooltip-popover:not(:popover-open) {
  opacity: 0;
  transform: translateY(-5px) scale(0.95);
}

/* 三角形の矢印 */
.tooltip-popover::before {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-bottom-color: #333;
}

ドロップダウンメニュー

<button popovertarget="menu-popover">メニュー</button>

<div id="menu-popover" popover class="menu-popover">
  <ul>
    <li><a href="#">プロフィール</a></li>
    <li><a href="#">設定</a></li>
    <li><a href="#">ヘルプ</a></li>
    <li><a href="#">ログアウト</a></li>
  </ul>
</div>
.menu-popover {
  padding: 0.5rem;
  min-width: 200px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);

  /* アニメーション */
  opacity: 1;
  transform: translateY(0) scale(1);
  transform-origin: top;
  transition:
    opacity 0.2s ease,
    transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
    overlay 0.2s allow-discrete,
    display 0.2s allow-discrete;
}

@starting-style {
  .menu-popover:popover-open {
    opacity: 0;
    transform: translateY(-10px) scale(0.9);
  }
}

.menu-popover:not(:popover-open) {
  opacity: 0;
  transform: translateY(-10px) scale(0.9);
}

.menu-popover ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.menu-popover li {
  margin: 0;
}

.menu-popover a {
  display: block;
  padding: 0.5rem 1rem;
  color: #333;
  text-decoration: none;
  border-radius: 4px;
  transition: background-color 0.15s;
}

.menu-popover a:hover {
  background-color: #f5f5f5;
}

トースト通知

基本トースト

<div id="toast-container"></div>

<button onclick="showToast('操作が完了しました')">トーストを表示</button>
function showToast(message, duration = 3000) {
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.textContent = message;

  document.getElementById('toast-container').appendChild(toast);

  setTimeout(() => {
    toast.classList.add('removing');
    setTimeout(() => toast.remove(), 300);
  }, duration);
}
#toast-container {
  position: fixed;
  bottom: 2rem;
  right: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 1000;
}

.toast {
  background: #333;
  color: white;
  padding: 1rem 1.5rem;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  min-width: 250px;

  /* アニメーション */
  opacity: 1;
  transform: translateX(0);
  transition:
    opacity 0.3s ease,
    transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

@starting-style {
  .toast {
    opacity: 0;
    transform: translateX(100px);
  }
}

.toast.removing {
  opacity: 0;
  transform: translateX(100px);
}

異なるタイプのトースト

/* 成功トースト */
.toast.success {
  background: #10b981;
}

@starting-style {
  .toast.success {
    opacity: 0;
    transform: translateY(20px) scale(0.9);
  }
}

/* エラートースト */
.toast.error {
  background: #ef4444;
  animation: shake 0.3s;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-10px); }
  75% { transform: translateX(10px); }
}

@starting-style {
  .toast.error {
    opacity: 0;
    transform: translateY(20px);
  }
}

/* 警告トースト */
.toast.warning {
  background: #f59e0b;
  color: #1f2937;
}

@starting-style {
  .toast.warning {
    opacity: 0;
    transform: scale(0.8);
  }
}

プログレス付きトースト

<div class="toast toast-progress">
  <p>ファイルをアップロード中...</p>
  <div class="progress-bar">
    <div class="progress-fill"></div>
  </div>
</div>
.toast-progress {
  padding: 1rem;
}

.toast-progress p {
  margin: 0 0 0.5rem 0;
}

.progress-bar {
  height: 4px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 2px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: white;
  width: 0;
  animation: progress 3s linear forwards;
}

@keyframes progress {
  to { width: 100%; }
}

@starting-style {
  .toast-progress {
    opacity: 0;
    transform: translateY(-20px);
  }
}

アコーディオン

<details class="accordion">
  <summary>セクション1</summary>
  <div class="accordion-content">
    <p>アコーディオンの内容がここに表示されます。</p>
  </div>
</details>
.accordion {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 0.5rem;
  overflow: hidden;
}

.accordion summary {
  padding: 1rem;
  cursor: pointer;
  user-select: none;
  background: #f9fafb;
  font-weight: 600;
  transition: background-color 0.2s;
}

.accordion summary:hover {
  background: #f3f4f6;
}

.accordion[open] summary {
  background: #e5e7eb;
}

.accordion-content {
  padding: 1rem;

  /* アニメーション */
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.3s ease,
    transform 0.3s ease;
}

@starting-style {
  .accordion[open] .accordion-content {
    opacity: 0;
    transform: translateY(-10px);
  }
}

カード入場アニメーション

<div class="card-grid">
  <div class="card" style="--delay: 0">カード1</div>
  <div class="card" style="--delay: 1">カード2</div>
  <div class="card" style="--delay: 2">カード3</div>
</div>
.card {
  background: white;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

  /* アニメーション */
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.5s ease calc(var(--delay) * 0.1s),
    transform 0.5s ease calc(var(--delay) * 0.1s);
}

@starting-style {
  .card {
    opacity: 0;
    transform: translateY(30px);
  }
}

ベストプラクティス

パフォーマンス最適化

/* transform と opacity のみを使用(GPU加速) */
.element {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s, transform 0.3s;
}

/* 避けるべき: width, height, top, left */
.element-bad {
  height: 100px; /* リフローを引き起こす */
  transition: height 0.3s;
}

アクセシビリティ

/* アニメーションを無効にするユーザー設定を尊重 */
@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }

  @starting-style {
    * {
      /* 初期状態を最終状態と同じにする */
      opacity: 1;
      transform: none;
    }
  }
}

タイミング関数

/* 自然な動き */
.natural {
  transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}

/* バウンス効果 */
.bounce {
  transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* 急激な開始 */
.snappy {
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

まとめ

@starting-styleを活用することで、以下のようなUIパターンを純粋なCSSだけで実装できます。

  • ダイアログ: モーダル、スライドイン
  • ポップオーバー: ツールチップ、ドロップダウンメニュー
  • トースト通知: 成功、エラー、警告、プログレス付き
  • アコーディオン: スムーズな展開・折りたたみ
  • カード: 段階的な入場アニメーション

JavaScriptを減らし、宣言的なCSSでアニメーションを管理することで、コードの保守性とパフォーマンスが向上します。ブラウザサポートも広がっているため、今すぐプロジェクトに導入できます。