本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
オーバーレイ

Cookie の同意バナーが下からせり上がり、選ぶと畳まれて消える

デモ

操作:クリック。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ページを開くと、画面の下の外に置いた同意バナーがせり上がり、中のアイコン・文言・ボタンが少し遅れて順に浮かびます。どちらかのボタンを押すと、中身が先に消え、バナーは上の縁から下の縁へ畳まれるように閉じながら、少し沈んで消えます。畳むのは切り抜き(clip-path)の上端を下げる動きで、箱の大きさは変えないので、中の文字や角の丸みが崩れません。

せり上がりは 0.9 秒の expo.out で、最初の 0.3 秒でほぼ出きり、残りの時間でゆっくり止まります。動きの大半を最初に済ませるので、読み始めるころにはバナーが落ち着いています。畳むのは 0.5 秒の power3.inOut で、中身は 0.18 秒で先に消し、畳みながら 16px 沈めます。出るときと消えるときで曲線を変えると、同じ道を戻るより、選び終えた区切りがはっきりします。

clip-path は、今の値から GSAP に読ませて補間すると崩れます。Chrome は inset(0% 0% 0% 0%) のような値を短い形に縮めて返すので、数の並びが行き先と合わず、角の丸みの数まで取り違えて補間され、畳む途中で角の形が変わります。畳んだ量を 1 つの CSS 変数にして clip-path の式に組み込んでおけば、GSAP はその数だけを補間できます。

選んだ結果は localStorage に保存し、次に来たときはバナーを出しません。保存名の末尾に版の数字を付けておくと、ポリシーを変えたときに数字を上げるだけで、全員にもう一度たずねられます。結果はページにイベントで知らせるので、アクセス解析などは「すべて許可」のときだけ読み込めます。足もとの「Cookieの設定」からはいつでも開き直せて、そのときは焦点をバナーのボタンへ移し、選んだら元の場所へ戻します。

画面の隅の小さなカードにするなら、幅を 400px ほどに絞って右下に寄せます。狭い画面では、ボタンを文言の下に横いっぱいに並べます。コーポレートサイトやネットショップ、メディアなど、アクセス解析や広告で Cookie を使うサイトに向いています。

コード

HTML
<!-- 同意バナー。hidden のまま置き、まだ選んでいない人にだけスクリプトが出す。
     body の最初に置くと、キーボードや読み上げで最初に届く -->
<section class="consent" aria-labelledby="consent-title" hidden>
  <svg class="consent__icon" viewBox="0 0 24 24" aria-hidden="true">
    <path d="M12 3a9 9 0 1 0 9 9 3 3 0 0 1-3-3 3 3 0 0 1-3-3 3 3 0 0 1-3-3z" />
    <circle cx="8" cy="10.5" r="0.9" /><circle cx="10.5" cy="16" r="0.9" /><circle cx="15.5" cy="14" r="0.9" />
  </svg>
  <div class="consent__text">
    <h2 class="consent__title" id="consent-title">Cookieの使用について</h2>
    <p class="consent__desc">
      当サイトでは、利便性の向上とアクセス解析のためにCookieを使用しています。詳しくは<a href="/cookie-policy/">Cookieポリシー</a>をご覧ください。
    </p>
  </div>
  <div class="consent__actions">
    <button class="consent__button" type="button" data-consent="necessary">必須のみ許可</button>
    <button class="consent__button consent__button--fill" type="button" data-consent="all">すべて許可</button>
  </div>
</section>

<!-- 選び直す口。フッターなどに置く(押すと、バナーがもう一度せり上がる) -->
<button class="consent-open" type="button" data-consent-open>Cookieの設定</button>

CSS
.consent,
.consent-open {
  --ink: #eeece6; /* 文字と線 */
  --mute: #a8a6a0; /* 説明の文字 */
  --surface: #262626; /* バナーの面 */
  --line: rgba(238, 236, 230, 0.14); /* 縁の罫線 */
  --ground: #111111; /* 主ボタンの文字(地の色) */
  --radius: 4px;
}

.consent {
  --fold: 0; /* 畳んだ量(0〜100)。スクリプトが動かす */
  position: fixed;
  right: 24px;
  bottom: 24px;
  left: 24px;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 24px;
  max-width: 1088px;
  margin: 0 auto;
  padding: 24px 32px;
  border-radius: var(--radius);
  background-color: var(--surface);
  box-shadow: inset 0 0 0 1px var(--line); /* 影は付けず、1px の罫線で縁を取る */
  color: var(--ink);
  /* 上の縁から下の縁へ畳む。数を 1 つの変数にしておくと、GSAP がそのまま補間できる */
  clip-path: inset(calc(var(--fold) * 1%) 0 0 0 round var(--radius));
}

.consent[hidden] { display: none; }

.consent__icon {
  flex: none;
  width: 32px;
  height: 32px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.consent__icon circle { fill: currentColor; }
.consent__text { flex: 1; min-width: 0; }
.consent__title { margin: 0; font-size: 16px; letter-spacing: 0.04em; }
.consent__desc { margin: 8px 0 0; color: var(--mute); font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; word-break: auto-phrase; }
.consent__desc a { color: var(--ink); text-underline-offset: 4px; white-space: nowrap; }
.consent__actions { display: flex; flex: none; gap: 16px; }

/* 副ボタンは文字の色の線、主ボタンは明るい塗りに地の色の文字 */
.consent__button {
  height: 48px;
  padding: 0 24px;
  border: 0;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--ink);
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
  cursor: pointer;
}

.consent__button--fill { box-shadow: none; background-color: var(--ink); color: var(--ground); }

.consent-open {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
}

.consent a:focus-visible,
.consent__button:focus-visible,
.consent-open:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }

/* 狭い画面では、ボタンを文言の下に横いっぱいに並べる */
@media (max-width: 720px) {
  .consent { right: 16px; bottom: 16px; left: 16px; flex-wrap: wrap; padding: 24px; }
  .consent__icon { display: none; }
  .consent__actions { width: 100%; }
  .consent__button { flex: 1; padding: 0 16px; }
}

JavaScript
import { gsap } from "gsap";

const KEY = "cookie-consent-v1"; // 選んだ結果の保存名。ポリシーを変えたら末尾の数字を上げると、全員にもう一度たずねる
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 20 : 1; // 動きを減らす設定では、ほぼ一瞬で切り替える

const banner = document.querySelector(".consent");
const items = banner.querySelectorAll(".consent__icon, .consent__text, .consent__button");
let motion = null;
let isOpen = false;
let returnTo = null; // 「Cookieの設定」から開いたときは、閉じたあと焦点をそこへ戻す

// ストレージを切っている環境でも止まらないように、読み書きの失敗は無視する
function load() {
  try {
    return localStorage.getItem(KEY);
  } catch {
    return null;
  }
}

function save(value) {
  try {
    localStorage.setItem(KEY, value);
  } catch {}
}

// 選んだ結果をほかのスクリプトへ知らせる。アクセス解析などは "all" のときだけ読み込む
// 例: document.addEventListener("cookieconsent", (event) => event.detail === "all" && loadAnalytics());
const announce = (value) => document.dispatchEvent(new CustomEvent("cookieconsent", { detail: value }));

const run = (timeline) => {
  motion?.kill();
  motion = timeline.timeScale(SPEED);
};

function show(from) {
  if (isOpen) return;
  isOpen = true;
  returnTo = from ?? null;
  banner.hidden = false;
  gsap.set(banner, { "--fold": 0 });
  run(
    gsap
      .timeline()
      // 画面の下の外から、勢いよく出てゆっくり止まる
      .fromTo(banner, { y: banner.offsetHeight + 24 }, { y: 0, duration: 0.9, ease: "expo.out" }, 0)
      .fromTo(items, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.6, ease: "power3.out", stagger: 0.06 }, 0.15)
  );
  if (from) banner.querySelector("button").focus({ preventScroll: true });
}

function choose(value) {
  if (!isOpen) return;
  isOpen = false;
  save(value);
  announce(value);
  const hadFocus = banner.contains(document.activeElement);
  run(
    gsap
      .timeline({
        onComplete: () => {
          banner.hidden = true;
          if (hadFocus) returnTo?.focus();
        },
      })
      // 中身を先に消してから、上の縁から下の縁へ畳みながら少し沈める
      .to(items, { opacity: 0, duration: 0.18, ease: "power1.in" }, 0)
      .to(banner, { "--fold": 100, duration: 0.5, ease: "power3.inOut" }, 0.1)
      .to(banner, { y: 16, duration: 0.5, ease: "power2.in" }, 0.1)
  );
}

banner.querySelectorAll("[data-consent]").forEach((button) => {
  button.addEventListener("click", () => choose(button.dataset.consent));
});
document.querySelectorAll("[data-consent-open]").forEach((button) => {
  button.addEventListener("click", () => show(button));
});

// 選んだ結果が保存されていれば、バナーは出さずに結果だけを知らせる
const saved = load();
if (saved) announce(saved);
else show();

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。