言うなればスライディングフォーカスを簡単に実現するライブラリ


マウスオーバーするとボーダーがひゅーんっと移動するメニューを時々見かけますが、これを実現するライブラリ(というか一個の関数)を作りました。こういうやつです。

…ちょっとフレームレートが低くてわかりにくいので 動くデモをご覧ください

私の探し方が悪いのか、この UI の呼び方がわからないので、とりあえずスライディングフォーカスと呼ぶことにします。
英語では Sliding Border Navigation Menu とか呼ばれることもあるようですが、正確にはボックスを移動していること、ナビゲーションやメニューに限定する必要はないことから、 Sliding Focus です。
そして今回作ったライブラリの名前は Flying Box JS です。紛らわしいですね。

動作確認環境

  • Mac の Chrome ・ Firefox ・ Safari
  • iPhone 5s の Safari
  • Windows の Edge

説明

シソーラス

  • アイテム要素
    メニューのアイテムに相当します。
  • フォーカス要素
    そのまま。
  • ホーム要素
    選択中のアイテム要素。現在のページなどに相当します。 正確には [data-is-home="true"] 属性が設定されたアイテム要素です。

何をしているのか

基本的には次の二点だけです。

  • アイテム要素がマウスオーバーされたらフォーカス要素の位置とサイズをアイテム要素に合わせる
  • クリックされたアイテム要素をホーム要素にする

ホーム要素と、ホーム要素に移動したフォーカス要素には、デフォルトで [data-is-home="true"] が設定されます。
後は CSS で上手く飾り付けてください。

最低限のサンプル

最低限のデモ のコードを掲載しておきます。
使い方としては flying-box.js を読み込み、 flyingBox() 関数を実行するだけです。
GitHubはこちら

<style>
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
  .flying-box {
    display: flex;
    justify-content: space-between;
    position: relative;
  }
  .flying-box a {
    display: block;
    flex-grow: 1;
    padding: 8px 0;
    text-align: center;
  }
  .flying-box a[data-is-home="true"] {
    color: #f00000;
  }
  .flying-box .flying-box__focus {
    background-color: rgba(0, 128, 240, 0.1);
    pointer-events: none;
    position: absolute;
    transition: all 100ms ease-out;
  }
  .flying-box .flying-box__focus[data-is-home="true"] {
    background-color: rgba(240, 0, 0, 0.1);
  }
</style>
<menu class="flying-box">
  <div class="flying-box__focus"></div>
  <a data-is-home="true">Home</a>
  <a>About</a>
  <a>Services</a>
  <a>Help</a>
</menu>
<script src="./flying-box.js"></script>
<script>
  flyingBox()
</script>

fliying-box.js はこうなっています。

const flyingBox = (option = {
  itemQuery: '.flying-box a',
  focusQuery: '.flying-box .flying-box__focus',
  homeAttr: 'data-is-home'
}) => {
  const focus = document.querySelector(option.focusQuery)
  const moveFocus = (item) => {
    if (!item) return
    focus.style['top'] = `${item.offsetTop}px`
    focus.style['left'] = `${item.offsetLeft}px`
    focus.style['width'] = `${item.clientWidth}px`
    focus.style['height'] = `${item.clientHeight}px`
    focus.setAttribute(option.homeAttr, item === queryHome())
  }
  const queryHome = () => document.querySelector(`${option.itemQuery}[${option.homeAttr}=true]`)
  moveFocus(queryHome())
  const items = document.querySelectorAll(option.itemQuery)
  items && items.forEach((item) => {
    item.addEventListener('mouseenter', () => { moveFocus(item) })
    item.addEventListener('mouseleave', () => { moveFocus(queryHome()) })
    item.addEventListener('click', () => {
      const home = queryHome()
      home && home.removeAttribute(option.homeAttr)
      item.setAttribute(option.homeAttr, true)
      focus.setAttribute(option.homeAttr, true)
    })
  })
}

ホーム要素が動的に切り替わることを考慮した結果、ちょっと冗長になりました。
タッチデバイスでは mouseenter とか click をタッチイベントに変えた方が良いかもしれません。

注意点

  • トランスパイルされる前提で書きました。
  • 「マウスオーバーではスライドせず、クリックした時だけスライドする」ような挙動の実装は…お任せします。
  • このスライディングフォーカス、実は CSS だけでも実装できますが、「各アイテムのサイズを決め打ちにしなければならない」「ラジオボタンなどを使わなければクリックで固定することができない」などの苦行が待ち受けているのでおすすめできません。 CSS だけで何とかしようとするのはやめましょう。

おわりに

UI の名前がわからない問題、どうにかしたい。