今や当たり前となったスクロールして要素をふわっと表示させるアレ。
- プラグインを利用したくない
- HTMLのdata属性でオプションを設定したい
というわけで、自作してみました!
仕様
単一要素への設定と複数要素への一括設定が可能です。
単一設定
- 表示したい要素にclass(
js-inview
)を設定します - 表示したい要素のdata属性(
data-inview-animation
)を設定します- data属性の値にCSSアニメーション名を設定します
- 画面と表示したい要素が交差した時にclass(
is-inview
)が付与されます
オプション
オプションは表示したい要素のdata属性に設定します。
data属性 | デフォルト | 設定内容 |
---|---|---|
data-inview-animation | なし(必須) | CSSアニメーション名 |
data-inview-delay | 0 | 交差してからの遅延秒数(ミリ秒) |
data-inview-options | { rootMargin: '0px'} | Intersection Observer のオプション |
data-inview-toggle | - | 交差するごとに表示・非表示を繰り返す |
data-inview-trigger | なし | 要素の表示をトリガーとして表示させたい要素 |
複数設定
- 表示したい要素の親要素にclass(
js-inviews
)を設定します - 表示したい要素の親要素にdata属性(
data-inview-targets-animation
)を設定します- data属性の値にCSSアニメーション名を設定します
- 表示したい要素にdata属性(
data-inview-target
)を設定します- data属性の値は設定しません
- 画面と表示したい要素が交差した時にclass(
is-inView
)が付与されます
オプション
表示させたい要素の親要素のdata属性に設定します。
data属性 | デフォルト | 設定内容 |
---|---|---|
data-inview-targets-animation | なし(必須) | 表示させたい子要素に設定するCSSアニメーション名 |
data-inview-delay | 0 | 交差してからの遅延秒数(ミリ秒) |
data-inview-delay-loop | 0 | 表示させたい子要素を連続して表示する間隔(ミリ秒) |
data-inview-options | { rootMargin: '0px'} | Intersection Observer のオプション |
data-inview-toggle | - | 交差するごとに表示・非表示を繰り返す |
複数設定した場合でも、表示させたい要素に単一設定のオプションを設定することで上書きが可能です。
CSS
css
/* 要素を非表示にする設定 */
[data-inview-animation],
[data-inview-target] {
opacity: 0;
}
/* アニメーション設定 */
[data-inview-animation='fade-in'].is-inView {
animation: fade-in 1s 0s forwards;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
JavaScript
JS
class Inview {
#defaults
#options
#inviewList
#inviewsList
constructor(config) {
this.#defaults = {
inviewSelector: '.js-inview',
inviewsSelector: '.js-inviews',
activeClass: 'is-inview',
rootMargin: '0px'
}
this.#options = { ...this.#defaults, ...config }
this.#inviewList = document.querySelectorAll(this.#options.inviewSelector)
this.#inviewsList = document.querySelectorAll(this.#options.inviewsSelector)
this.#init()
}
#init() {
this.#single()
this.#multiple()
}
// 単一要素を表示
#single() {
for (const target of this.#inviewList) {
const callback = (entries) => {
for (const entry of entries) {
const isInviewToggle = target.dataset.inviewToggle === ''
// 画面と要素が交差
if (entry.isIntersecting) {
this.#show(target)
this.#showTrigger(target)
// target.dataset.inviewToggle属性がない場合
// 表示後は監視を解除
if (!isInviewToggle) {
observer.unobserve(target)
}
}
// target.dataset.inviewToggle属性がある場合
// 画面内から消えたら要素を非表示
if (!entry.isIntersecting && isInviewToggle) {
this.#hide(target)
}
}
}
const observer = new IntersectionObserver(
callback,
this.#getOptions(target)
)
observer.observe(target)
}
}
// 複数要素を表示
#multiple() {
for (const inviewsItem of this.#inviewsList) {
const inviewTargets = inviewsItem.querySelectorAll('[data-inview-target]')
.length
? inviewsItem.querySelectorAll('[data-inview-target]')
: inviewsItem.querySelectorAll('[data-inview-animation]')
for (const target of inviewTargets) {
this.#setAnimation(target)
const inviewsTarget = inviewsItem.dataset.inviewDelayLoop
? inviewsItem
: target
const callback = (entries) => {
entries.forEach((entry, index) => {
// 画面と要素が交差
if (entry.isIntersecting) {
// inview-delay-loopが設定されていたら
if (inviewsItem.dataset.inviewDelayLoop) {
inviewTargets.forEach((item, index) => {
this.#show(item, index)
this.#showTrigger(item)
})
}
// inview-delay-loop設定なし
else {
this.#show(target, index)
this.#showTrigger(target)
}
// target.dataset.inviewToggle属性がない場合
// 表示後は監視を解除
if (!this.#isInviewToggle(target)) {
observer.unobserve(target)
}
}
// target.dataset.inviewToggle属性がある場合
// 画面内から消えたら要素を非表示
if (!entry.isIntersecting && this.#isInviewToggle(target)) {
this.#hide(target)
}
})
}
const observer = new IntersectionObserver(
callback,
this.#getOptions(inviewsTarget)
)
observer.observe(target)
}
}
}
// アニメーション対象となる子要素に
// アニメーション設定用のdata属性を付与
#setAnimation(el) {
const closest = el.closest(this.#options.inviewsSelector)
if (!el.dataset.inviewAnimation && closest.dataset.inviewTargetsAnimation) {
el.dataset.inviewAnimation = closest.dataset.inviewTargetsAnimation
}
}
// 交差するごとに表示・非表示を繰り返す
#isInviewToggle(el) {
const closest = el.closest(this.#options.inviewsSelector)
if (
el.dataset.inviewToggle === '' ||
(closest && closest.dataset.inviewToggle === '')
) {
return true
}
}
// Intersection Observer のオプションを返す
#getOptions(el) {
const closest = el.closest(this.#options.inviewsSelector)
let options = null
if (el.dataset.inviewOptions) {
options = el.dataset.inviewOptions
} else if (closest && closest.dataset.inviewOptions) {
options = closest.dataset.inviewOptions
}
return options ? JSON.parse(options) : this.#options
}
// 要素が見えてからの表示タイミング(ミリ秒)を返す
#getDelay(el, index) {
const closest = el.closest(this.#options.inviewsSelector)
// 単一要素を表示
if (el.dataset.inviewDelay) {
return el.dataset.inviewDelay
}
// 複数要素を表示
if (closest) {
// 親要素にdata-inview-delay-loop属性あり
if (closest.dataset.inviewDelayLoop) {
// 親要素にdata-inview-delay属性あり
if (closest.dataset.inviewDelay) {
if (index === 0) {
return closest.dataset.inviewDelay
}
return (
closest.dataset.inviewDelayLoop * index +
parseInt(closest.dataset.inviewDelay)
)
}
// 親要素にdata-inview-delay属性なし
return closest.dataset.inviewDelayLoop * index
}
// 親要素にdata-inview-delay属性あり
if (closest.dataset.inviewDelay) {
return closest.dataset.inviewDelay
}
}
}
// 要素にclassを付与して表示させる
#show(el, index) {
const duration = this.#getDelay(el, index)
let startTime = null
const timer = (currentTime) => {
if (!startTime) startTime = currentTime
const progress = Math.floor(currentTime - startTime)
if (progress < duration) {
window.requestAnimationFrame(timer)
} else {
el.classList.add(this.#options.activeClass)
}
}
window.requestAnimationFrame(timer)
}
// 要素のclassを削除して非表示にする
#hide(el) {
el.classList.remove(this.#options.activeClass)
}
// 要素の表示と同時に指定した要素にclassを付与して表示させる
#showTrigger(el) {
if (el.dataset.inviewTrigger) {
const triggers = document.querySelectorAll(el.dataset.inviewTrigger)
for (const trigger of triggers) {
this.#show(trigger)
}
}
}
}
const inview = new Inview()
DEMO
See the Pen show element on scroll by Takuya Mori (@taqumo) on CodePen.