今更ではありますが、 Bootstrap4 のナビゲーションバーの折り畳み (collapse) の開閉アニメーションをカスタマイズしたくなったのでやってみました。
成果物
今回は横からのドロワーにしてみました。
動機
Bootstrap4 のデフォルトの挙動はNavbar ・ Bootstrap v4.6やナビゲーションバー~Bootstrap4移行ガイド等で確認できます。
お馴染みの上下方向に折り畳みするアニメーションですね。ちなみに、このアニメーションは折り畳み (collapse) で実現しているようです。
そこでカスタマイズする方法を調べてみると【Bootstrap】細かすぎて使い道がないかもしれないBootstrap4小技集10選 | Wood-Roots:blogがヒットしました。
ただしこの方法では横に開く場合でもheightを取得、設定する処理自体は発生するため、Collapseを開くタイミングがワンテンポ遅れます。この点を回避したい場合はソースを修正して再ビルドする必要があります。
【Bootstrap】細かすぎて使い道がないかもしれないBootstrap4小技集10選 | Wood-Roots:blog
記事内にこのような記載があり、確かに折り畳み (collapse) のアニメーションに干渉するにはJSのソースから手を入れなければならないか……?と思いつつ、それ以外の方法がないか模索してみることにしました。
調査
デモ等で何度も折り畳み (collapse) のアニメーションを動作させると、
- 折り畳んだ状態 (デフォルト状態、または
.navbar-toggler
に.collapsed
が付与かつaria-expanded="false"
で、navbar-collapse .collapse
に.show
が付与されていない状態) - 遷移状態 (
navbar-collapse .collapse
に.collapsing
が付与されている状態) - 展開状態 (
.navbar-toggler
に.collapsed
が付与されていないかつaria-expanded="true"
で、navbar-collapse .collapse
に.show
が付与されている状態)
この3つの状態を行き来してることが分かりました。
最初は css transition で干渉しようかと思いましたが、この状態遷移ではJSも絡んでくること、また、アニメーションの内容によっては充分に制御することができない場合もあったので断念しました (opacity: 0;
と opacity: 1;
を transition すると、展開時はスムーズにアニメーションが開始するが、折り畳み時はトグルボタンをクリックしてから若干 (デフォルトの折り畳みを実行する時間, 0.2~0.3秒程?) のタイムラグが発生し、もっさりした動きにかじられる、といった具合)。
そこで別の方法を模索。
折り畳み (collapse) にはイベントがAPIとして公開されているため、これを使ってデフォルトの挙動を強制的に無効化して、クラスの付与・削除のみを実行する処理に置き換えてしまうことにしました。
コード
ということで、コード。
<nav class="navbar navbar-expand-lg navbar-light bg-white fixed-top l-header p-0" id="navbar">
<div class="container">
<a class="navbar-brand my-0" href="./"><%= commons.sitename %></a>
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarList" aria-controls="navbarList" aria-expanded="false" aria-label="ナビゲーションの切替">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarList">
<ul class="nav navbar-nav ml-xl-auto">
<li class="nav-item">
<a class="nav-link" href="#">hoge</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">piyo</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">fuga</a>
</li>
</ul>
</div>
</nav>
例えばこんなナビゲーションバーのコンポーネントを用意します。
$(() => {
if($('#navbarList').length > 0 && $('#navbar .navbar-toggler').length > 0) {
// navbar にメニューとハンバーガーアイコンが存在する場合
const $navbarList = $('#navbarList');
const $navbarToggler = $('#navbar .navbar-toggler');
$navbarList.on('show.bs.collapse', function () {
// 展開時のデフォルトの挙動を潰して Collapse の結果のクラスの付与・削除のみ実施
$navbarToggler.removeClass('collapsed');
$navbarList.addClass('show');
})
$navbarList.on('hide.bs.collapse', function () {
// 格納時のデフォルトの挙動を潰して Collapse の結果のクラスの付与・削除のみ実施
$navbarToggler.addClass('collapsed');
$navbarList.removeClass('show');
});
}
});
JS (jQuery) はこのような感じで。 show.bs.collapse
, hide.bs.collapse
のイベントトリガーに関数を付けて、処理を上書きしています。
後は単純にクラスの付け外しです。
.navbar {
.container-fluid {
position: relative; //ドロワーのため
}
&-collapse {
width: 100%;
@media (max-width: 1199px) {
min-height: calc(100vh - 60px);
transition: opacity 0.3s ease
background-color: #fff;
color: #333;
padding: 2rem 3rem;
position: absolute;
top: 60px;
transition: left 0.4s ease;
&:not(.show) {
display: block;
left: 100vw;
}
&.show {
left: 0;
}
.navbar-nav {
width: 100%;
}
.nav-item {
padding: 1rem;
&:hover,
&:active,
&:focus {
background-color: #000;
transition: color 0.3s ease, background-color 0.3s ease;
}
.nav-link {
font-size: 1.25rem;
&,
&:link,
&:hover,
&:active,
&:focus {
color: #fff;
text-decoration: none;
}
}
}
}
}
}
デザイン的な装飾も混ざっていますが、ドロワーのアニメーションとしてはクラスの付け外しに伴って css transition で left: 100vw;
から left: 0;
に .navbar-nav
を移動させているだけです。
後はドロワーにするために Bootstrap4 のコンテナの padding 等と干渉を避けるようにしたり。
ハンバーガーアイコンのアニメーション
基本はCSSで実装するハンバーガーメニュークリック時のエフェクト 10+ – NxWorldを参考に。
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarList" aria-controls="navbarList" aria-expanded="false" aria-label="ナビゲーションの切替">
<span></span>
<span></span>
<span></span>
</button>
HTML は span
を3つ用意。
@use "sass:map";
@use "sass:math";
$hamburger: (
button-wh: 2.75rem,
icon-width: 2rem,
icon-height: 2px,
margin: 0.5rem,
top: 0.75rem
);
@keyframes hamburger-bar1 {
0% {
transform: translateY(map.get($hamburger, margin)) rotate(45deg);
}
50% {
transform: translateY(map.get($hamburger, margin)) rotate(0);
}
100% {
transform: translateY(0) rotate(0);
}
}
@keyframes active-hamburger-bar1 {
0% {
transform: translateY(0) rotate(0);
}
50% {
transform: translateY(map.get($hamburger, margin)) rotate(0);
}
100% {
transform: translateY(map.get($hamburger, margin)) rotate(45deg);
}
}
@keyframes hamburger-bar3 {
0% {
transform: translateY(calc(-1 * map.get($hamburger, margin))) rotate(-45deg);
}
50% {
transform: translateY(calc(-1 * map.get($hamburger, margin))) rotate(0);
}
100% {
transform: translateY(0) rotate(0);
}
}
@keyframes active-hamburger-bar3 {
0% {
transform: translateY(0) rotate(0);
}
50% {
transform: translateY(calc(-1 * map.get($hamburger, margin))) rotate(0);
}
100% {
transform: translateY(calc(-1 * map.get($hamburger, margin))) rotate(-45deg);
}
}
.navbar {
& .navbar-toggler {
border-radius: 0;
border-color: transparent;
position: relative;
width: map.get($hamburger, button-wh);
height: map.get($hamburger, button-wh);
&:focus {
outline: 1px solid #333; // outline カスタマイズ
}
span {
width: map.get($hamburger, icon-width);
height: map.get($hamburger, icon-height);
background-color: #333;
display: block;
position: absolute;
left: math.div((map.get($hamburger, button-wh) - map.get($hamburger, icon-width)), 2);
&:first-of-type {
top: map.get($hamburger, top);
}
&:nth-of-type(2) {
top: calc(map.get($hamburger, top) + map.get($hamburger, margin));
}
&:last-of-type {
top: calc(map.get($hamburger, top) + map.get($hamburger, margin) * 2);
}
}
&.collapsed {
span {
&:first-of-type {
animation: hamburger-bar1 0.5s forwards;
}
&:nth-of-type(2) {
opacity: 1;
}
&:last-of-type {
animation: hamburger-bar3 0.5s forwards;
}
}
}
&.active {
span {
&:first-of-type {
animation: active-hamburger-bar1 0.5s forwards;
}
&:nth-of-type(2) {
opacity: 0;
}
&:last-of-type {
animation: active-hamburger-bar3 0.5s forwards;
}
}
}
}
}
本題とは関係ないですが、ハンバーガーアイコン周りの数値はわりと緻密に計算したかったので Scss の配列としてまとめました。
$(() => {
if($('#navbarList').length > 0 && $('#navbar .navbar-toggler').length > 0) {
// navbar にメニューとハンバーガーアイコンが存在する場合
const $navbarList = $('#navbarList');
const $navbarToggler = $('#navbar .navbar-toggler');
$navbarList.on('show.bs.collapse', function () {
// 展開時のデフォルトの挙動を潰して Collapse の結果のクラスの付与・削除のみ実施
$navbarToggler.removeClass('collapsed');
$navbarToggler.addClass('active');
$navbarList.addClass('show');
})
$navbarList.on('hide.bs.collapse', function () {
// 格納時のデフォルトの挙動を潰して Collapse の結果のクラスの付与・削除のみ実施
$navbarToggler.addClass('collapsed');
$navbarToggler.removeClass('active');
$navbarList.removeClass('show');
});
}
});
JS は1つ .active
の付け外しを追加。これはハンバーガーアイコンで :not(.collapsed)
と collapsed
のトグルのみだと、ページの初回表示時にもハンバーガーを綴じるアニメーションが走ってしまうのでそれを抑制したくて別のクラスを付与することにしました。
これでもブレークポイントをまたがって行き来した際のアニメーションは抑制できていないので不完全ではありますが、一応対処ということで。
参考
Bootstrap4, Navbarコンポーネント
Bootstrap4, 折り畳み (collapse)
折り畳み (collapse) のカスタマイズ
Bootstrap4, 折り畳み (collapse) のイベント
Bootstrap4, 折り畳み (collapse) のJS
css transition
- transition – CSS: カスケーディングスタイルシート | MDN
- Bootstrap のCollapse の開閉速度を調整する方法 | WEBデザインのTIPSまとめサイト「ウェブアンテナ」
.collapsing
で制御しようとした話 (ただし記事は Bootstrap3 前提)