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

ページの上端からお知らせのバーが降りてきて、閉じると畳まれてページが詰まる

デモ

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

仕様

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

しくみと調整

ページが開くと、上端の外からお知らせのバーが降りてきて、ページ全体をバーの高さだけ押し下げます。右端の × を押すと、バーの高さが 0 へ畳まれて、ページが上へ詰まります。閉じたことはブラウザに覚えさせて次に来たときは出さず、ヘッダーの鈴のボタンからいつでももう一度出せます。

バーは画面に重ねず、ページの流れの中(body の先頭)に置きます。重ねるとナビやページの頭を隠してしまいますが、流れの中なら高さを変えるだけで、下の中身が押し下げられ、畳めば詰まります。降りるのは 0.6 秒の power3.out、畳むのは 0.5 秒の power3.inOut で、中身は降り始めの 0.15 秒後から 0.06 秒ずつずらして浮かべます。

中身はバーの下の縁にそろえて置きます。バーを overflow で切り抜き、縦並びの flex で justify-content を flex-end にすると、高さが足りない間は中身の上が切れるので、降りるときは上から現れ、畳むときは上へ巻き取られるように見えます。上の縁にそろえたままだと、中身がその場で下から切られていくだけで、降りてくる感じが出ません。高さは auto まで補間するので、狭い画面で文が折り返しても、そのまま動きます。

閉じると × が消えるので、焦点を鈴のボタンへ移します。移さないと、キーボードで操作している人の焦点がページの頭へ戻ってしまいます。バーの中に焦点があるときは Esc でも閉じられます。鈴のボタンには aria-expanded を付け、バーが出ているかを読み上げで伝えます。保存名をお知らせごとに替えると、新しいお知らせは全員にまた出ます。

ヘッダーを画面に固定しているサイトでは、バーの高さだけヘッダーとページの上の余白を下げます。地をブランドの色にしたり、文を 2 行にしてバーを高くしたりしても、同じ作りで動きます。キャンペーンやセールの告知、新機能のお知らせ、メンテナンスの予告に向いています。

コード

HTML
<!-- お知らせのバー。body の最初に置き、ページの流れの中で上端に出す(重ねないので、出るとページが下がる)。
     hidden のまま置き、まだ閉じていない人にだけスクリプトが出す -->
<div class="announce" id="announce" role="region" aria-label="お知らせ" hidden>
  <div class="announce__inner">
    <span class="announce__badge">期間限定</span>
    <p class="announce__text">年額プランが9月30日まで20%オフになります。</p>
    <a class="announce__link" href="/pricing/">詳しく見る</a>
    <button class="announce__close" type="button" aria-label="お知らせを閉じる">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6L6 18" /></svg>
    </button>
  </div>
</div>

<!-- もう一度出す口。ヘッダーなどに置く -->
<button class="announce-open" type="button" aria-label="お知らせを表示" aria-controls="announce" aria-expanded="false" data-announce-open>
  <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <path d="M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z" /><path d="M10 20.5a2 2 0 0 0 4 0" />
  </svg>
</button>

CSS
.announce,
.announce-open {
  --ink: #262626; /* ページ側のボタンの線 */
  --bar: #445c6c; /* バーの地(墨のスレート) */
  --on-bar: #ffffff; /* バーの文字 */
  --soft: rgba(255, 255, 255, 0.16); /* 札の地と、× に乗ったときの地 */
}

/* 中身を下の縁にそろえる。高さが足りない間は上が切れるので、上から降りてきて、上へ巻き取られるように見える */
.announce {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
  background-color: var(--bar);
  color: var(--on-bar);
}

.announce[hidden] { display: none; }

.announce__inner {
  position: relative;
  display: flex;
  flex: none;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 16px;
  min-height: 48px;
  padding: 8px 64px;
  box-sizing: border-box;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: 0.04em;
  text-align: center;
}

.announce__badge {
  padding: 2px 10px;
  border-radius: 999px;
  background-color: var(--soft);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.announce__text { margin: 0; word-break: auto-phrase; } /* 狭い画面で 2 行になっても、文節で折り返す */
.announce__link { color: inherit; font-weight: 600; text-underline-offset: 4px; white-space: nowrap; }

.announce__close,
.announce-open {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}

/* × はバーの右端。上下の中央に置く */
.announce__close { position: absolute; top: calc(50% - 20px); right: 16px; }
.announce__close:hover { background-color: var(--soft); }
.announce-open { box-shadow: inset 0 0 0 1px var(--ink); color: var(--ink); } /* 線の丸 */

.announce svg,
.announce-open svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.announce a:focus-visible,
.announce__close:focus-visible { outline: 2px solid var(--on-bar); outline-offset: 2px; }
.announce-open:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* 狭い画面では、札を外して文とリンクだけにする(2 行に折り返してもよい) */
@media (max-width: 600px) {
  .announce__inner { padding: 8px 56px 8px 16px; justify-content: flex-start; font-size: 13px; text-align: left; }
  .announce__badge { display: none; }
  .announce__close { right: 8px; }
}

JavaScript
import { gsap } from "gsap";

const KEY = "announce-autumn-2026"; // 閉じたことの保存名。お知らせを替えたら、名前も替える(全員にまた出る)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 20 : 1; // 動きを減らす設定では、ほぼ一瞬で切り替える

const bar = document.querySelector(".announce");
const items = bar.querySelectorAll(".announce__inner > *");
const openers = document.querySelectorAll("[data-announce-open]");
let motion = null;
let isOpen = false;

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

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

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

function show() {
  if (isOpen) return;
  isOpen = true;
  bar.hidden = false;
  openers.forEach((button) => button.setAttribute("aria-expanded", "true"));
  run(
    gsap
      .timeline()
      // 高さ 0 から中身の高さまで広げる。バーはページの流れの中にあるので、下の中身が押し下げられる
      .fromTo(bar, { height: 0 }, { height: "auto", duration: 0.6, ease: "power3.out" }, 0)
      .fromTo(items, { opacity: 0 }, { opacity: 1, duration: 0.25, ease: "power1.out", stagger: 0.06 }, 0.15)
  );
}

function hide() {
  if (!isOpen) return;
  isOpen = false;
  save("closed");
  openers.forEach((button) => button.setAttribute("aria-expanded", "false"));
  // 閉じた × は消えるので、焦点を「もう一度出す」ボタンへ移す(無ければページの頭から読み直しになる)
  if (bar.contains(document.activeElement)) openers[0]?.focus();
  run(
    gsap.timeline({ onComplete: () => (bar.hidden = true) })
      // 中身は消さずに、下の縁ごと上へ抜けていく。ページは上へ詰まる
      .to(bar, { height: 0, duration: 0.5, ease: "power3.inOut" })
  );
}

bar.querySelector(".announce__close").addEventListener("click", hide);
openers.forEach((button) => button.addEventListener("click", () => (isOpen ? hide() : show())));
// バーの中に焦点があるときは、Esc でも閉じる
bar.addEventListener("keydown", (event) => event.key === "Escape" && hide());

// まだ閉じていなければ、ページが開いて少ししてから降ろす(最初の描画と同時だと、動きに気づかれない)
if (load() !== "closed") gsap.delayedCall(reduceMotion ? 0 : 0.4, show);

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

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