そのコンテンツがスクロール可能であることをユーザーに知らせるためのスクロールヒントを、CSSだけで実装する方法を考えてみました。
この記事では、モダンブラウザで利用できるようになってきたCSSのスクロール駆動アニメーションを活用して、スクロール可能な方向を示す2つの方法を紹介します。
- フェードアウト — コンテンツの端をグラデーションでフェードさせ、続きがあることを暗示する。
- シャドウ — スクロール可能な方向に影を表示し、奥行きで続きを示す。
どちらのケースでも、スクロールに合わせてフェードやシャドウの方向を切り替えるようにしていきます。JavaScriptは使いません。
スクロール方向に連動するフェードアウトの実装
コンテンツの端を透明にフェードさせることで、「この先にも続きがある」ことを視覚的に伝えます。
スクロールさせたい要素に.u-scFade:x(横方向)または.u-scFade:y(縦方向)クラスを付与して使えるように、次のようなCSSを用意します。
/*
* @property でCSS変数をアニメーション可能にする
* syntax: '<number>' で数値として扱い、アニメーションを有効化する
*/
@property --scFade-progress {
syntax: '<number>';
inherits: false;
initial-value: -1;
}
/* フェードアウトさせるための変数アニメーション */
@keyframes scFade {
0% {
--scFade-progress: -1;
}
100% {
--scFade-progress: 0;
}
}
.u-scFade\:x,
.u-scFade\:y {
--scFade-size: 2rem;
--scFade-offset: 1px; /* borderがついていたらうっすら見えたりするので、offsetで調節可能に。 */
/* 両端が消えていくマスクグラデーションを作成 */
mask-image: linear-gradient(
var(--scFade-dir),
transparent var(--scFade-offset),
black var(--scFade-size),
black calc(100% - var(--scFade-size)),
transparent calc(100% - var(--scFade-offset))
);
}
/*
* マスク自体は両端が消えるものになっているが、消えて欲しいのは片方だけ。
* なので、mask-sizeを 100% + フェードサイズ にして はみ出るように配置し、スクロールに合わせて位置をずらす。
*/
.u-scFade\:x {
--scFade-dir: to right;
mask-size: calc(100% + var(--scFade-size)) 100%;
mask-position: calc(var(--scFade-progress) * var(--scFade-size)) 0;
animation: scFade linear;
animation-timeline: scroll(self x);
}
.u-scFade\:y {
--scFade-dir: to bottom;
mask-size: 100% calc(100% + var(--scFade-size));
mask-position: 0 calc(var(--scFade-progress) * var(--scFade-size));
animation: scFade linear;
animation-timeline: scroll(self y);
} サンプル
<div class="u-scFade:x l--flex -g:20 -ov-x:auto"> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div></div>※ CSSが書かれていないクラスはLism CSSのものです。
縦方向のスクロールにも対応しています。
<div class="u-scFade:y l--stack -g:20 -ar:16/9 -ov-y:auto"> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div></div>※ CSSが書かれていないクラスはLism CSSのものです。
仕組み
mask-imageプロパティでグラデーションマスクを適用し、両端を透明に、中央を不透明にすることでフェードアウト効果を作っています。
ポイントは、マスクのサイズを要素よりも少し大きくして、スクロール位置に応じてマスクの位置をずらすことです。これにより、スクロールの先頭では右端だけがフェードし、末尾では左端だけがフェードします。
スクロール位置に連動したアニメーションにはanimation-timeline: scroll(self x|y)を使用しています。これはCSS単独でスクロール駆動アニメーションを実現する新しいプロパティです。
2025年12月現在、Firefoxが非対応のままですので注意が必要ですが、スクロールヒントに関しては動かなくても何か表示が崩れるものではないので導入しても大丈夫かと思います。
また、CSS変数をアニメーションさせるために@propertyでカスタムプロパティを数値型として定義しています。
スクロール方向に連動するシャドウの実装
スクロール可能な方向を示すために、要素の内側に影(insetなbox-shadow)を表示する方法です。
スクロール要素に.u-scShadow:x(横方向)または.u-scShadow:y(縦方向)クラスを付与し、さらに**position: relativeを持つ親要素でラップ**してください。
/*
* @property でCSS変数をアニメーション可能にする
* syntax: '<number>' で数値として扱い、アニメーションを有効化する
*/
@property --scShadowX {
syntax: '<number>';
inherits: false;
initial-value: -1;
}
@property --scShadowY {
syntax: '<number>';
inherits: false;
initial-value: -1;
}
/* Xサイズ(方向)に乗算する数値をアニメーションで逆転させる */
@keyframes scShadowX {
from {
--scShadowX: -1;
}
to {
--scShadowX: 1;
}
}
/* Yサイズ(方向)に乗算する数値をアニメーションで逆転させる */
@keyframes scShadowY {
from {
--scShadowY: -1;
}
to {
--scShadowY: 1;
}
}
/**
* スクロール位置に連動して、スクロール可能な方向に影を表示する。
* inset な box-shadow は、内部コンテンツより下側に表示されるため、擬似要素で子要素より上で影を表示する。
* この時、普通に .u-scShadow を基準に absolute 配置するとスクロールで一緒に流れていってしまう。
* さらに親要素を基準にして擬似要素を配置する必要があるため、 position:static とする。
*
* scroll-timelineを定義して、親のスクロールを基準にして擬似要素のシャドウを動かす。
*/
.u-scShadow\:x,
.u-scShadow\:y {
position: static;
/* 自由に変更してください */
--scShadow-c: hsl(240 4% 8% / 50%); /* 影の色 */
--scShadow-size: 2rem; /* 影のサイズ */
}
/* 擬似要素の共通スタイル */
.u-scShadow\:x::after,
.u-scShadow\:y::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
/* 計算値 */
--_shadowX: calc(var(--scShadow-size) * var(--scShadowX));
--_shadowY: calc(var(--scShadow-size) * var(--scShadowY));
--_spread: calc(var(--scShadow-size) * -1);
/* シャドウセット */
box-shadow: inset var(--_shadowX) var(--_shadowY) var(--scShadow-size) var(--_spread) var(--scShadow-c);
}
/* X方向スクロール用 */
.u-scShadow\:x {
scroll-timeline: --scShadowX-timeline x;
}
.u-scShadow\:x::after {
/* Y方向のシャドウは不要 */
--scShadowY: 0;
/* --scShadowX が -1 ~ 1 の間で変化することで、X方向のシャドウの向きが変わっていく */
animation: scShadowX linear;
animation-timeline: --scShadowX-timeline; /* 親で定義したタイムラインを参照 */
}
/* Y方向スクロール用 */
.u-scShadow\:y {
scroll-timeline: --scShadowY-timeline y;
}
.u-scShadow\:y::after {
/* X方向のシャドウは不要 */
--scShadowX: 0;
/* --scShadowY が -1 ~ 1 の間で変化することで、Y方向のシャドウの向きが変わっていく */
animation: scShadowY linear;
animation-timeline: --scShadowY-timeline; /* 親で定義したタイムラインを参照 */
} サンプル
<div class="-pos:rel"> <div class="u-scShadow:x l--flex -g:20 -ov-x:auto"> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> <div class="l--center -ar:16/9 -bgc:base-2 -fxsh:0" style='width:20rem'>Box</div> </div></div>※ CSSが書かれていないクラスはLism CSSのものです。
縦方向のスクロールにも対応しています。
<div class="-pos:rel"> <div class="u-scShadow:y l--stack -g:20 -ar:16/9 -ov-y:auto"> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> <div class="l--center -py:50 -bgc:base-2 -fxsh:0">Box</div> </div></div>※ CSSが書かれていないクラスはLism CSSのものです。
仕組み
スクロール位置に連動して、影の位置と方向を変化させています。スクロールが先頭にあるときは右側(または下側)に影が表示され、末尾に近づくと左側(または上側)に影が移動します。
この実装では擬似要素を使って影を表示しています。理由は、insetなbox-shadowは要素の内側に描画されるため、内部コンテンツの下に隠れてしまうことがあるためです。擬似要素を使うことで、コンテンツより上のレイヤーに影を配置できます。
スクロールに連動したアニメーションにはscroll-timelineプロパティを使用し、擬似要素のアニメーションからそのタイムラインを参照しています。