上書きされていくテキスト
概要
文章が切り替わる際に、次の文章が上からディゾルプして表示されるアニメーションです。
複数の文章を重ね合わせ、 文章にマスクをつけて、そのマスクの大きさを変動させることで上から文章が徐々に表示される動きを表現しています。文章は背景色を指定することで、背面にある文章は徐々に消えていくようにしました。文章の重ね順はクラス名によって、z-index
を操作して調整しています。
JavaScriptで現在表示されているものをis-current
、前回表示されていたものをis-prev
というクラス名を操作しています。
テキストだけでなく、ページ自体をディゾルプで切り替えたりもできるアニメーションです。
コードサンプル
HTML
<div class="screen-text">
<p class="screen-text__item is-current">TEXT</p>
<p class="screen-text__item">SCREEN</p>
<p class="screen-text__item">SAMPLE</p>
<button class="screen-btnarea__btn screen-btnarea__btn--prev" aria-label="前へ" type="button">
<svg viewBox="0 0 24 24" width="50%" height="50%" aria-hidden="true">
<path d="M15 18l-6-6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button class="screen-btnarea__btn screen-btnarea__btn--next" aria-label="次へ" type="button">
<svg viewBox="0 0 24 24" width="50%" height="50%" aria-hidden="true">
<path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
CSS
.screen-text {
/* アニメーションの時間 */
--duration: 2.5s;
/* 境界のぼかし範囲 */
--blur: 10%;
/* 色 */
--bg-color: #f4f5f9;
--font-color: #333333;
--btn-color: #08254f;
--btn-hv-color: #7d8797;
position: relative;
display: grid;
width: 100%;
grid-template: "text" auto / 1fr;
place-items: center;
background-color: var(--bg-color);
}
.screen-text__item {
grid-area: text;
width: 100%;
color: var(--font-color);
background-color: inherit;
text-align: center;
/* マスク(黒=表示/透明=非表示) */
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - var(--blur)), rgba(0, 0, 0, 0) 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - var(--blur)), rgba(0, 0, 0, 0) 100%);
-webkit-mask-size: 100% 0%; /* 縦0% → 100%に広げて露出 */
mask-size: 100% 100%;
will-change: mask-size;
}
/* 現在表示中のもの(静止時の見た目) */
.screen-text__item.is-current {
animation: screen-reveal var(--duration) cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
z-index: 2;
}
@keyframes screen-reveal {
from {
mask-size: 100% 0%;
}
to {
mask-size: 100% 100%;
}
}
/* 前回表示したもの */
.screen-text__item.is-prev {
z-index: 1;
}
/* --- ボタンのCSS --- */
.screen-btnarea__btn {
display: grid;
place-items: center;
font-size: inherit;
color: #F4F5F9;
width: 0.8em;
height: 0.8em;
background: var(--btn-color);
border: 1px solid;
border-radius: 50%;
transition: background-color 0.3s;
z-index: 5;
}
.screen-btnarea__btn:focus-visible {
background-color: var(--btn-hv-color);
color: #F4F5F9;
}
@media (any-hover) {
.screen-btnarea__btn:hover {
background-color: var(--btn-hv-color);
color: #F4F5F9;
}
}
.screen-btnarea__btn--prev {
position: absolute;
inset-inline-start: 1em;
inset-block: 0;
margin-block: auto;
}
.screen-btnarea__btn--next {
position: absolute;
inset-inline-end: 1em;
inset-block: 0;
margin-block: auto;
}
JavaScript
document.addEventListener("DOMContentLoaded", () => {
const wrap = document.querySelector(".screen-text");
if (!wrap) return;
const items = Array.from(wrap.querySelectorAll(".screen-text__item"));
const prevBtn = document.querySelector(".screen-btnarea__btn--prev");
const nextBtn = document.querySelector(".screen-btnarea__btn--next");
// 初期の current を決定
let current = items.findIndex(el => el.classList.contains("is-current"));
if (current {
if (el.classList.contains("is-prev")) {
el.classList.remove("is-prev");
}
});
// 現在の要素を is-prev に
items[current]?.classList.remove("is-current");
items[current]?.classList.add("is-prev");
// 新しい要素を is-current に
items[newIndex]?.classList.add("is-current");
current = newIndex;
}
function next() {
setCurrent((current + 1) % items.length);
}
function prev() {
setCurrent((current - 1 + items.length) % items.length);
}
nextBtn?.addEventListener("click", next);
prevBtn?.addEventListener("click", prev);
// キーボード操作(左右)
document.addEventListener("keydown", e => {
if (e.key === "ArrowRight") next();
if (e.key === "ArrowLeft") prev();
});
});