【CSS + JS】テキストの無限ループアニメーション
レスポンシブなドロップダウンメニューをつくる
目次
ドロップダウンメニューって?
メニューをクリックしたりマウスを上に載せたりすることでそのメニューの下に出てくるようなメニューのことです。
ドロップダウンメニューの実装方法はググると結構出てきます。
ただjQueryプラグインとかをこのためだけに読み込ませたりするのはなんか違う気がするし、「CSSだけでできる!」とかいうのもありますが、実際は動いてもデザインが思い通りにならなかったり、タブレットで全然使い物にならなかったり、なんてことがありますね。
僕なんかは特にそうですが、ほぼ「コーディング専業」の人は依頼されたデザインに合わせて自由自在に変えられるように、このくらいのUIはササっと書ける知識を持っておく必要があります。
今回はそういう時に使えるメモです。
言うて自分もけっこう忘れるので。w
まずはデザインをhtml・cssで実装
基本的にはデザイナーから依頼されたデザインを完全に再現するという体で進めます。 そのためCSSでデザインを忠実に再現することが前提となります。
今回はデザイナーなしで自分で作りますがご容赦ください。
html
<nav class="nav" id="js-nav">
<ul class="nav-inner">
<li class="nav-item">
<a href="<?php the_permalink()?>">TOP</a>
</li>
<!-- /.nav-item -->
<li class="nav-item">
<a href="<?php the_permalink()?>">ABOUT</a>
</li>
<!-- /.nav-item -->
<li class="nav-item nav-dropdown js-dropdown">
<a href="<?php the_permalink()?>">CATEGORY</a>
<button type="button" class="nav-toggle js-toggle"></button>
<ul class="dropdown">
<li class="dropdown-item has-child js-has-child">
<a href="<?php the_permalink()?>">CATEGORY A</a>
<button type="button" class="nav-toggle js-toggle"></button>
<ul class="dropdown-child">
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">A-ITEM 1</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">A-ITEM 2</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">A-ITEM 2</a>
</li>
<!-- /.dropdown-child-item -->
</ul>
<!-- /.dropdown-child -->
</li>
<!-- /.dropdown-item -->
<li class="dropdown-item has-child js-has-child">
<a href="<?php the_permalink()?>">CATEGORY B</a>
<button type="button" class="nav-toggle js-toggle"></button>
<ul class="dropdown-child">
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 1</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 2</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 3</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 4</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 5</a>
</li>
<!-- /.dropdown-child-item -->
<li class="dropdown-child-item">
<a href="<?php the_permalink()?>">B-ITEM 6</a>
</li>
<!-- /.dropdown-child-item -->
</ul>
<!-- /.dropdown-child -->
</li>
<!-- /.dropdown-item -->
<li class="dropdown-item">
<a href="<?php the_permalink()?>">CATEGORY C</a>
</li>
<!-- /.dropdown-item -->
</ul>
<!-- /.dropdown -->
</li>
<!-- /.nav-item -->
<li class="nav-item">
<a href="<?php the_permalink()?>">CONTACT</a>
</li>
<!-- /.nav-item -->
</ul>
<!-- /.nav-inner -->
</nav>
<!-- /.nav -->
このコードはWordPressでデモ用に適当に作ったのでリンクが全てthe_permalink()
になっちゃってますが、今回は動きだけの紹介ですので気にしないでください。笑
css
CSSはリセットされている前提で進めます。
//ブレイクポイントを変数で設定
$bp_tb_v: 768px; //タブレット縦
$bp_tb_h: 960px; //タブレット横・PC
.nav{
@include min-screen($bp_tb_v){
display: flex;
justify-content: center;
}
}
.nav-inner{
background: #eee;
@include min-screen($bp_tb_v){
display: flex;
justify-content: center;
flex-wrap: wrap;
width: 100%;
max-width: 1080px;
margin-right: auto;
margin-left: auto;
padding:9px;
background: #eee;
border-radius: 9px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
}
.nav-item{
position: relative;
@include min-screen($bp_tb_v){
width: 25%;
}
&.is-show > a{
@include min-screen($bp_tb_v){
background: #ddd;
}
}
+ .nav-item{
border-top:1px solid #fff;
@include min-screen($bp_tb_v){
border-top: none;
}
}
&.is-show,
&:hover {
.dropdown{
@include min-screen($bp_tb_v){
opacity: 1;
visibility: visible;
}
&::before{
@include min-screen($bp_tb_v){
transform:none;
}
}
}
}
> a{
padding: 15px;
font-size: 18px;
@include min-screen($bp_tb_v){
display: flex;
align-items: center;
justify-content: center;
padding: 21px 30px;
font-size: 15px;
border-radius: 9px;
}
@include min-screen($bp_tb_h) {
min-width:180px;
font-size: 18px;
}
&:hover{
@include min-screen($bp_tb_v){
background: #ddd;
}
}
}
}
.nav-dropdown{
display: flex;
align-items: center;
flex-wrap: wrap;
@include min-screen($bp_tb_v){
display: block;
}
a{
flex-grow: 1;
}
}
.nav-toggle{
position: relative;
width: 49px;
min-height:49px;
background: $blue;
@include min-screen($bp_tb_v){
display: none;
}
&::before,
&::after{
content:"";
position: absolute;
top:0;
right:0;
bottom:0;
left:0;
display: block;
margin:auto;
background: #fff;
transition:.2s ease;
}
&::before{
width:15px;
height:1px;
}
&::after{
width:1px;
height:15px;
}
&.is-open{
&::before{
opacity: 0;
transform:rotate(90deg);
}
&::after{
transform:rotate(90deg);
}
}
}
.dropdown{
display: none;
width: 100%;
background: rgba(#fff,.5);
border-top:1px solid #fff;
@include min-screen($bp_tb_v){
opacity: 0;
visibility: hidden;
position: absolute;
top: 100%;
left: 0;
z-index: 10;
display: block;
width: 100%;
padding: 30px 0 0;
background: none;
border-top: none;
border-radius:9px;
transition:.2s ease;
}
&::before{
@include min-screen($bp_tb_v){
content:"";
position: absolute;
top:-9px;
right:0;
left:0;
z-index: 10;
display: block;
width: 1px;
height: 30px;
margin:auto;
background: #000;
transform-origin: top center;
transform:scaleY(0);
transition:.2s ease-out;
}
}
}
.dropdown-item{
@include min-screen($bp_tb_v){
position: relative;
}
+ .dropdown-item {
border-top: 1px solid #fff;
@include min-screen($bp_tb_v){
border-top: none;
}
}
&.has-child{
display: flex;
flex-wrap: wrap;
@include min-screen($bp_tb_v){
position: relative;
display: block;
}
&::after{
@include min-screen($bp_tb_v){
opacity: 0;
visibility: hidden;
content:"";
position: absolute;
top:0;
right:-22px;
z-index: 10;
bottom:0;
display: block;
width: 30px;
height: 1px;
margin:auto;
background: #000;
transform-origin: left center;
transform: scaleX(.5);
transition:.2s ease-out;
}
}
}
&.is-show,
&:hover{
&::after{
@include min-screen($bp_tb_v){
opacity: 1;
visibility: visible;
transform: none;
}
}
.dropdown-child{
@include min-screen($bp_tb_v){
opacity: 1;
visibility: visible;
}
}
}
&.is-show {
> a{
@include min-screen($bp_tb_v){
color:#fff;
background: $blue;
}
}
}
a{
padding:15px;
font-size: 15px;
@include min-screen($bp_tb_v){
padding:18px 30px;
text-align: center;
border-radius:9px;
}
&:hover{
@include min-screen($bp_tb_v){
color:#fff;
background: $blue;
}
}
}
}
.dropdown-child{
display: none;
width: 100%;
border-top:1px solid #fff;
@include min-screen($bp_tb_v){
opacity: 0;
visibility: hidden;
position: absolute;
top:0;
left:100%;
display: block;
padding-left:15px;
background: none;
border-top: none;
transition:.2s ease;
}
}
.dropdown-child-item{
background: rgba(#fff,.3);
@include min-screen($bp_tb_v){
background: none;
}
+ .dropdown-child-item{
border-top: 1px solid #fff;
@include min-screen($bp_tb_v){
border-top: none;
}
}
a{
display: inline-block;
font-size: 12px;
@include min-screen($bp_tb_v){
display: block;
font-size: 15px;
}
}
}
全部書いてしまいましたが、
第一段階としてPCではCSSの:hover
で動くようにだけしておきます。
JSで追加するクラスのCSSに関してはその都度書いていきます。
基本的にこういった様々なサイトでよく使うようなUIを作る時はCSSのみで解決できる方が好ましいと思っています。
JavaScript全盛の時代ではありますが、Vueなどのフレームワークを使っても結局JSで操作してCSSやCSSの付いたクラスを付け替えることが大半なので:hover
や:focus
といった擬似クラスが使える場合はそちらを優先して使った方が効率的に思います。
そのためこういった小規模UIを自作で作るときは先にCSSだけで動かすことを最初に考えます。
しかしレスポンシブがスタンダードになった最近では、スマホやタブレットといったマウスのないタッチ操作デバイスの対応は違ってきます。 スマホやタブレットでは逆にJSを使わないと制御できない部分が多くなります。
このドロップダウンメニューもCSSのみではスマホやタブレットでうまく機能しないのでJavaScriptでのカスタマイズが必要です。
ともあれひとまず最初の段階でCSSで見た目を作り込んでおくと次のJS実装がやりやすいと思います。
JavaScriptでタップ操作に対応する
スマホ・タブレット・PCで違う動作をするのでちょっとややこしくなりますが、分けて考えていきます。
サンプルはjQuery(ES6+をBabel7でトランスパイル)で書きます。
スマホ
スマホではタップして開くアコーディオンメニューにします。
横の「+」ボタンを押すと下のカテゴリを開いたり閉じたりします。
const $toggleBtns = $('.js-toggle');
$.each($toggleBtns, (i, el) => {
el.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
const $self = $(e.currentTarget);
$self.toggleClass('is-open');
$self.next().not(':animated').slideToggle(200);
}, { passive: false });
});
.js-toggle
が「+」ボタンです。
タップにはtouchstart
イベントを使ってタッチした瞬間に発火させます。addEventListener
を使っていますが、これで引数に{ passive: false }
を付けていないとpreventDefault
を使った時に怒られます。jQueryのon
では{ passive: false }
は付けられないので、リンクの動作をJSで制御する時などはaddEventListener
を使いましょう。
タップするとその直後の要素をnext()
で取得し、slideToggle()
を使ってメニューを開閉します。連打バグ防止のために.not(':animated')
でアニメーションしていない時のみ動作するようにします。
「+」ボタンはCSSでタブレットの大きさになるとdisplay:none;で消えるように設定しているので、ここでは「スマホだけ動作するような判定処理」はしません。
タブレット
タブレットではPCと同じデザインになりますが、動作をマウスからタップに置き換えます。
1度目のタップでメニューを開いてメニューを開いた状態で同じリンクをタップするとリンクに飛ばす仕様です。また、メニューが開いた状態でメニュー以外をタップするとメニューを閉じます。
ここはスマホでもPCでも動作して欲しくない処理なので、「タブレットのみ」で動作するようにデバイス判定します。
デバイス判定
//デバイス判定
device(){
const device = {
isSp : false,
isTablet : false,
isPC : false
};
const ua = navigator.userAgent;
//スマホ
if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 || (ua.indexOf('Android') > 0) && (ua.indexOf('Mobile') > 0) || ua.indexOf('Windows Phone') > 0){
device.isSp = true;
}
//タブレット
else if(ua.indexOf('iPad') > 0 || ua.indexOf('Android') > 0){
device.isTablet = true;
}
//PC
else{
device.isPC = true;
}
return device;
}
親カテゴリのドロップダウン
デバイス判定の関数を使って、次に親カテゴリの動作。
const dropdown = document.querySelector('.js-dropdown');
dropdown.addEventListener('touchstart', (e) => {
const device = device();
if (!device.isTablet) {
return;
}
const $dropdown = $(e.currentTarget);
if($dropdown.hasClass('is-show')){
return;
}
else {
e.preventDefault();
e.stopPropagation();
$dropdown.addClass('is-show');
}
}, false);
タブレットもタップするのでtouchstart
でイベントを登録します。
最初にデバイス判定してタブレット以外の場合はreturnで終了させます。
タブレットの時のみ、.is-show
クラスがない時につけるようにします。
この時、.is-show
クラスがある場合はそのままreturnで終了し、ない場合はリンクの機能を止めてドロップダウンを開きたいのでpreventDefault()
を使います。stopPropagation()
はイベントの伝播を止めたい場合に使います。今回はなくても動きますが一応書いています。
子カテゴリのドロップダウン
//子カテゴリ
const $childDropdown = $('.js-has-child');
$.each($childDropdown, (i,el) => {
el.addEventListener('touchstart', (e) => {
const device = device();
if (!device.isTablet) {
return;
}
const $dropdown = $(e.currentTarget);
if($dropdown.hasClass('is-show')){
return;
}
else {
e.preventDefault();
e.stopPropagation();
$childDropdown.removeClass('is-show');
$dropdown.addClass('is-show');
}
}, false);
});
子カテゴリも親カテゴリと挙動は同じです。
ドロップダウンさせたいカテゴリに.js-has-child
というクラスを付けておいて、それに対して動作させます。
親カテゴリと違う点は、
- 複数要素に対してイベントを発火させる
- 1つのカテゴリが表示されたらそれ以外のカテゴリはすべて非表示にする
という2点です。
エリア外をタップすると閉じる
document.addEventListener('touchstart', (e) => {
const device = device();
if (!device.isTablet) {
return;
}
if(!$('.js-dropdown').hasClass('is-show')){
return;
}
if(!$(e.target).closest('.js-dropdown').length) {
e.preventDefault();
e.stopPropagation();
$('.js-dropdown').removeClass('is-show');
$('.js-has-child').removeClass('is-show');
}
}, { passive: false });
document
にtouchstart
イベントを追加します。
これでどこをタップしてもイベントが発火する形になります。
タブレット以外の時やドロップダウンが開いていない時はすぐに終了。
タップした要素(e.target
)の先祖に.js-dropdown
が含まれていない場合にすべてのドロップダウンを閉じます。
その際にやはりpreventDefault()
でリンクを機能させないようにしておきます。
これでタブレットで「1度目のタップでメニューを開いてメニューを開いた状態で同じリンクをタップするとリンクに飛ばす、またメニューが開いた状態でメニュー以外をタップするとメニューを閉じる」というちょっと複雑な条件をクリアできました。
JS全体
Classにまとめたもの
class Main {
constructor(){
this.bind()
}
//デバイス判定
device(){
const device = {
isSp : false,
isTablet : false,
isPC : false
};
const ua = navigator.userAgent;
//スマホ
if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 || (ua.indexOf('Android') > 0) && (ua.indexOf('Mobile') > 0) || ua.indexOf('Windows Phone') > 0){
device.isSp = true;
}
//タブレット
else if(ua.indexOf('iPad') > 0 || ua.indexOf('Android') > 0){
device.isTablet = true;
}
//PC
else{
device.isPC = true;
}
return device;
}
bind(){
//カテゴリ
const dropdown = document.querySelector('.js-dropdown');
dropdown.addEventListener('touchstart', (e) => {
const device = this.device();
if (!device.isTablet) {
return;
}
const $dropdown = $(e.currentTarget);
if($dropdown.hasClass('is-show')){
return;
}
else {
e.preventDefault();
e.stopPropagation();
$dropdown.addClass('is-show');
}
}, false);
//子カテゴリ
const $childDropdown = $('.js-has-child');
$.each($childDropdown, (i,el) => {
el.addEventListener('touchstart', (e) => {
const device = this.device();
if (!device.isTablet) {
return;
}
const $dropdown = $(e.currentTarget);
if($dropdown.hasClass('is-show')){
return;
}
else {
e.preventDefault();
e.stopPropagation();
$childDropdown.removeClass('is-show');
$dropdown.addClass('is-show');
}
}, false);
});
document.addEventListener('touchstart', (e) => {
const device = this.device();
if (!device.isTablet) {
return;
}
if(!$('.js-dropdown').hasClass('is-show')){
return;
}
if(!$(e.target).closest('.js-dropdown').length) {
e.preventDefault();
e.stopPropagation();
$('.js-dropdown').removeClass('is-show');
$('.js-has-child').removeClass('is-show');
}
}, { passive: false });
const $toggleBtns = $('.js-toggle');
$.each($toggleBtns, (i, el) => {
el.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
const $self = $(e.currentTarget);
$self.toggleClass('is-open');
$self.next().not(':animated').slideToggle(200);
}, { passive: false });
});
}
}
new Main();
レスポンシブなドロップダウンメニューのDEMO
どのデバイスでも動作するドロップダウンメニューができました。
デザインに関してはイケてないので参考にしないでくださいw
おわりに
最近よく作っているBASEデザインマーケットで販売しているテーマで、「タブレットのタップ操作がうまくいかない」といったご指摘を頂いて何度か修正しました。 その時に行ったこととほとんど同じ処理なのですが、しばらくすると忘れそうなのでここに書き残しておくことにしました。
ただこんなUI、Vueとかで作った方が早そうな気がしますね。
最近はjQueryは嫌いって人たちがかなり多いと思うので、また時間がある時にVueやネイティブバージョンも作ってみます。
RELATED
NEW POSTS
【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた
【netlify】ビルドイメージを更新 [ Ubuntu Xenial 16.04 → Ubuntu Focal 20.04 ]
【PHP】Google Business Profile APIを使ってクチコミを取得する
スクリーンショットのAPI「screendot」を使ってみた
【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定
lax.jsの使い方【スクロール連動アニメーションの実装】