Nuxt.jsでユーザビリティを考慮したスムーススクロールを実現する方法

JS や jQuery で実装していたスムーススクロール機能を Vue.js/Nuxt.js で実現するのに苦戦していませんか?

ライブラリを使うことでスムーススクロールの実装自体は容易です。というよりは一瞬でできます。 しかし、URL にアンカーが付かない(/#element)という大きな問題がありました。いくつかのライブラリを導入してみましたがいずれも対応されておらず、それではユーザビリティを損ねるということで改善策を検討しました。

Nuxt.js を使うことでモダンに開発できるのはいいことですが、昔から重宝されていた(当たり前とされていた)機能が実現できないということが往々にして発生しがちですので、そこを如何にクリアにしていくかが肝となります。

昔からよく使われている実装例

$(function () {
  $('a[href^="#"]').click(function () {
    var speed = 500
    var href = $(this).attr('href')
    var target = $(href == '#' || href == '' ? 'html' : href)
    var position = target.offset().top
    $('html, body').animate({ scrollTop: position }, speed, 'swing')
    return false
  })
})

簡単ですね。これだけで <a href="#element">見出し1</a> といったページ内リンクでのスムーススクロールが実現できています。 なお、見出し 1 のアンカーテキストをクリックすると URL は http://example.com/#element に変化しますので飛び先の状態で第三者に URL を共有することができます。

Vue.js/Nuxt.js におけるスムーススクロール

前述のコードは jQuery を使用していますが、Vue.js や Nuxt.js ではそもそも jQuery を使わないようにしているケースが多いと思われます。 Vue.js でスムーススクロールするためのライブラリがいくつもありますので、基本的にはそのライブラリにお世話になっている方が多いのではないでしょうか。

 vue-smooth-scroll

問題点:URL が変化しない

<a href="#" v-scroll-to="'#element'">見出し1</a>
...
<h2 id="element">
</h2>

この状態で見出し 1 をクリックすると確かにスムーススクロールが実現できることが分かります。 しかし、URL に#elementが付与されないため、遷移先の状態で URL を人に共有することができません(http://example.com/ のまま)。 これは利用者のユーザビリティを大きく損ねることに繋がりますので避けたい事象です。

とは言ってもイチからライブラリの開発をするのもな…ということで検討した結果、以下のような拡張プラグインを作成しました。 ※内部的に vue-scrollto を使用しています。

解決策

vue-scrollto は活用させていただき、それを wrapper させたプラグインを作成しました。 やっていることは単純で、クリックイベントを止めて直接 vue-scrollto の内部関数をコールするようにしていること、カスタムディレクティブの場合も同様でクリックイベントをリッスンして直接 vue-scrollto を実行しています。

公開用ではないため冗長な記述もありますのでご容赦ください。 そのままでも使えると思いますので、興味がありましたら使ってみて下さい。 Nuxt.js をよりユーザーフレンドリーにするための一助となれば幸いです。

コンポーネント

使いやすくするため、カスタムディレクティブにも対応しています。

カスタムディレクティブ(Vue.js 公式)

<a v-smooth-scroll="{duration: 300}" href="#element" class="">some text</a>

↑ のような書き方でも OK。 ちなみに、どのような場合であっても<nuxt-link>では機能しませんので<a>タグをご使用ください。

<a
  href="#element"
  @click.stop="$smoothScroll.scrollTo($event.srcElement.hash, 300)"
  >some text</a
>

また、細かな制御をしたいとき用に、Programmatically な書き方もできます。

scrollTo (e): void {
  this.$smoothScroll.scrollTo(e.srcElement.hash, 300)
}

プラグイン(plugins/smooth-scroll.ts)

import VueScrollTo, { ScrollOptions } from 'vue-scrollto'
import { Plugin } from '@nuxt/types'

import Vue, { VNode } from 'vue'
import { DirectiveBinding } from 'vue/types/options'

type ElementDescriptor = Element | string
interface VueSmoothScrollHTMLElement extends HTMLElement {
  vueSmoothScrollDuration?: number;
  vueSmoothScrollOptions?: ScrollOptions;
}

export interface ScrollToInterface {
  (hash: ElementDescriptor, duration: number, options?: ScrollOptions): void;
}

export interface SmoothScrollInterface {
  scrollTo: ScrollToInterface;
}

/**
 * クリックイベント発火時にコールされる関数
 * @param event From AddEventListener
 */
function scrollToFunction(event: any): void {
  VueScrollTo.scrollTo(
    event.srcElement.hash,
    event.target.vueSmoothScrollDuration,
    event.target.vueSmoothScrollOptions
  )
}

const smoothScroll: Plugin = (_context, inject) => {
  const scrollTo: ScrollToInterface = (hash, duration, options): void => {
    VueScrollTo.scrollTo(hash, duration, options)
  }

  const smoothScroll: SmoothScrollInterface = {
    scrollTo,
  }

  inject('smoothScroll', smoothScroll)

  Vue.directive('smooth-scroll', {
    bind: (
      el: VueSmoothScrollHTMLElement,
      binding: DirectiveBinding,
      _vnode: VNode,
      _oldVnode: VNode
    ): void => {
      el.addEventListener('click', scrollToFunction, false)
      el.vueSmoothScrollDuration = binding.value.duration
      el.vueSmoothScrollOptions = binding.value.options
    },
    unbind: (
      el: VueSmoothScrollHTMLElement,
      _binding: DirectiveBinding,
      _vnode: VNode,
      _oldVnode: VNode
    ): void => {
      el.removeEventListener('click', scrollToFunction, false)
    },
  })
}

export default smoothScroll

nuxt.config.js

plugins: [
  ...
  { src: '@/plugins/smooth-scroll.ts' }
],
  ...
modules: [
  ...
  ['vue-scrollto/nuxt', { duration: 300 }]
]

関連記事