【Slick.js】タブエリアをスワイプして自由に他のタブを押す

Slickを使ってタブ切り替えをすると、タブエリアをスワイプした時に次のタブに移動します。
次のタブに移動せず、自由に他のタブを押せるように出来ないかなと思い、SCSSと組み合わせてやってみました。

<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="rNqgxwr" data-user="t-kawaguchi" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/t-kawaguchi/pen/rNqgxwr">
  qiita_20230529_slick</a> by t-kawaguchi (<a href="https://codepen.io/t-kawaguchi">@t-kawaguchi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


## HTML

タブ用とコンテンツ用のスライダーの元となるHTMLをそれぞれ準備し、タブのスライダー`<ul class="js-tabs slide__tabs"></ul>`を`.slide__tabs__wrap`で囲います。

```html
<!-- タブ -->
<div class="slide__tabs__wrap">
	<ul class="js-tabs slide__tabs">
		<li class="tab">Tab1</li>
		<li class="tab">Tab2</li>
		<li class="tab">Tab3</li>
		<li class="tab">Tab4</li>
		<li class="tab">Tab5</li>
		<li class="tab">Tab6</li>
		<li class="tab">Tab7</li>
		<li class="tab">Tab8</li>
		<li class="tab">Tab9</li>
		<li class="tab">Tab10</li>
	</ul>
</div>
<!-- コンテンツ -->
<div class="js-slider slide__list">
	<div class="js-list1 item">
		<p>Tab1の中身</p>
	</div>
	<div class="js-list2 item">
		<p>Tab2の中身</p>
	</div>
	<div class="js-list3 item">
		<p>Tab3の中身</p>
	</div>
	<div class="js-list4 item">
		<p>Tab4の中身</p>
	</div>
	<div class="js-list5 item">
		<p>Tab5の中身</p>
	</div>
	<div class="js-list6 item">
		<p>Tab6の中身</p>
	</div>
	<div class="js-list7 item">
		<p>Tab7の中身</p>
	</div>
	<div class="js-list8 item">
		<p>Tab8の中身</p>
	</div>
	<div class="js-list9 item">
		<p>Tab9の中身</p>
	</div>
	<div class="js-list10 item">
		<p>Tab10の中身</p>
	</div>
</div>
```

## SCSS

タブスライダーの外側の`.slide__tabs__wrap`と、タブスライダー内に自動で組み込まれる`.slick-list`を横スクロールできるようにすることで、自由にスワイプすることができるようにします。

```scss
.slide__tabs {
    overflow-x: scroll; // 横スクロール可能に
}
```
```scss
// スワイプするための設定
.slick-list {
    overflow-x: scroll; // 横スクロール可能に
    // PC用にスクロールバーを非表示
    -ms-overflow-style: none;
    scrollbar-width: none;
    &::-webkit-scrollbar { 
        display: none;
    }
}
.slick-track {
    // タブエリアの横幅に合わせる
    width: max-content !important;
    // カレントタブが左端に固定にならないようにリセット
    transform: none !important;
}
```

### 全コード

```scss
// 読み込み時にslickが縦並びになるのを防ぐ
.slide__tabs,
.slide__list {
    opacity: 0;
    transition: opacity 0.3s linear;
}
.slick-initialized {
    opacity: 1;
}

// タブ
// ========================================================
.slide__tabs {
    padding: 8px 7px 0;
    width: max-content;
    &__wrap {
        position: sticky;
        top: 0;
        z-index: 1;
        overflow-x: scroll; // 横スクロール可能に
        border-bottom: 3px solid #eee;
        transition: All 0.2s ease;
        background: #fff;
    }
    .tab {
        width: 120px;
        margin-top: 5px;
        padding: 12px 10px;
        min-width: 56px;
        border: 1px solid #ddd;
        border-bottom: none;
        border-radius: 5px 5px 0 0;
        color: #666;
        text-align: center;
    }
    .slick-active {
        &.tab {
            margin-top: 0;
            padding: 15px 10px;
            border: none;
            background: #eee;
            color: #333;
            font-weight: bold;
        }
    }
    .slick-slide:not(:last-child) {
        margin-right: 5px;
    }
    // スワイプするための設定
    .slick-list {
        overflow-x: scroll; // 横スクロール可能に
		// PC用にスクロールバーを非表示
		-ms-overflow-style: none;
		scrollbar-width: none;
		&::-webkit-scrollbar { 
			display: none;
		}
    }
    .slick-track {
		// タブエリアの横幅に合わせる
        width: max-content !important;
		// カレントタブが左端に固定にならないようにリセット
        transform: none !important;
    }

}
// コンテンツ
// ========================================================
.slide__list {
    .item {
      padding: 50px;
		p {
			background: #eee;
			height: 120vh;
			display: flex;
			justify-content: center;
			align-items: center;
		}
    }
}
```


## JS

タブとコンテンツのスライダーを`asNavFor`のオプションで連動させます。
タブのスワイプはCSSの横スクロールを優先したいので`draggable`と`swipe`のオプションをfalseにし、どちらもループはさせないので`infinite`もfalseにします。

```javascript
$('.js-tabs').slick({
	asNavFor: '.js-slider',
	draggable: false,
	infinite: false,
	swipe: false,
});
$('.js-slider').slick({
	asNavFor: '.js-tabs',
	infinite: false,
});
```

タブをクリックした時に、タブをウィンドウの中央に表示させたいのでSlickの`beforeChange`イベントを使って処理を入れます。
また、スライドが切り替わった時に、コンテンツを一番上から表示させたいのでトップに移動させます。

```javascript
$tabs.on('beforeChange', function(event, slick, currentSlide, nextSlide) {
	const body_width = $(window).innerWidth(); // ウィンドウ幅
	const $active_tag = $('[data-slick-index="' + nextSlide + '"]'); // 選択したタブ
	const active_tab_width = $active_tag.innerWidth(); // 選択したタブの幅
	const active_tab_position = $active_tag.position().left; // 選択したタブのX座標
	const scroll_left = active_tab_position - (body_width - active_tab_width) / 2; // 横スクロール量
	// 選択したタブが画面の中央に表示されるようスクロール
	$('.slide__tabs__wrap').animate({scrollLeft: scroll_left}, 300, 'swing');
	// コンテンツが一番上から表示されるようにトップに移動
	$('body,html').scrollTop(0);
});
```

### 全コード

```javascript
$(function() {
	// slick
	const $tabs = $('.js-tabs');
	const $slider = $('.js-slider');
	$tabs.slick({
		arrows: false,
		asNavFor: '.js-slider',
		draggable: false,
		focusOnSelect: true,
		infinite: false,
		swipe: false,
		variableWidth: true,
	});
	$slider.slick({
		arrows: false,
		asNavFor: '.js-tabs',
		infinite: false,
		touchThreshold: 10,
	});

	$tabs.on('beforeChange', function(event, slick, currentSlide, nextSlide) {
		const body_width = $(window).innerWidth(); // ウィンドウ幅
		const $active_tag = $('[data-slick-index="' + nextSlide + '"]'); // 選択したタブ
		const active_tab_width = $active_tag.innerWidth(); // 選択したタブの幅
		const active_tab_position = $active_tag.position().left; // 選択したタブのX座標
		const scroll_left = active_tab_position - (body_width - active_tab_width) / 2; // 横スクロール量
		// 選択したタブが画面の中央に表示されるようスクロール
		$('.slide__tabs__wrap').animate({scrollLeft: scroll_left}, 300, 'swing');
		// コンテンツが一番上から表示されるようにトップに移動
		$('body,html').scrollTop(0);
	});
});
```

スマホに実装する前提で調整したので、PC表示は最低限しか整えてません。
レスポンシブサイトで使うときはメディアクエリで表示を切り替えるなどの工夫が必要かと思います。