実際は以下のようにsm md等のレスポンシブな指定が含まれたものも出力します。
TailwindCSSのみを使用したホバータイプのドロップダウンメニューを作成します。
以前の記事では、クリックによって表示非表示が切り替わるドロップダウンメニューを紹介しました。
今回は、CSSのみで実現するホバーメニューに対して以下の点に焦点をあてて記述していきます。
以下の手順で記述します。
環境に関しては、TailwindCSSのみを使用し以下のようになっています。
項目 | バージョン |
---|---|
TailwindCSS | 1.2.0 |
まず素のCSSで確認を行い、その後TailwindCSSでどう実現するかという段取りで進めます。
CSSのみで実現するドロップダウンメニューの実現方法として、最小限の実装を通して確認します。
実現方法の確認なので、要素の見た目は度外視です。
まず、以下のように要素を作ります。
簡単のため、body
の下には全部 div
要素を配置することにします。
<body>
<div class="dropdown">
<div>
Menu
</div>
<div class="menu">
<div>Item1</div>
<div>Item2</div>
<div>Item3</div>
</div>
</div>
</body>
ここで .menu
以下の要素は通常は表示せず、Menu
がホバーされたときのみ表示されるようにします。
CSSは以下のようになります。
.dropdown {
position: relative;
}
.menu {
position: absolute;
visibility: hidden;
}
.dropdown:hover .menu { visibility: visible;}
上記でハイライトされている箇所がポイントです。
親要素が hover
されているときに、.menu
が指定された子要素が表示されます。
動作が確認できるデモを CodePen に置きました。
以下から確認できます。
ここでは、わかりやすさのため背景色を指定しています。
TailwindCSS では、上記を実現するのにgroup-hoverを使います。
group-hoverに関する説明は、本家サイトの以下の部分に記述があります。
https://tailwindcss.com/docs/pseudo-class-variants/#group-hover
TailwindCSS では、上記を指定するのにちょっと細工が必要になります。
まず、tailwind.config.js
が存在していなければ、作成します。
TailwindCSSがプロジェクトにインストールされている場合は、以下のコマンドで作成できます。
$ npx tailwind init
作成されたファイルに対して、以下のハイライト部分を追加します。
デフォルトの出力に加えて最後尾に group-hover
を追加しています。
今回は表示状態を変更したいので、visibility
に対してのみ指定しています。
// tailwind.config.js
module.exports = {
theme: {
extend: {},
},
variants: {
visibility: ['responsive', 'hover', 'focus', 'group-hover'] },
plugins: [],
}
上記を指定し、コマンドを通じてCSSを生成すると、以下のクラスが自動生成されます。
.group:hover .group-hover\:visible {
visibility: visible;
}
.group:hover .group-hover\:invisible {
visibility: hidden;
}
.group:hover .sm\:group-hover\:visible {
visibility: visible;
}
これらを踏まえて、素のCSSで指定したものと等価なものは、TailwindCSSでは以下のようになります。
<body>
<div class="relative group bg-red-100">
<div>
Menu
</div>
<div class="absolute invisible group-hover:visible bg-green-100">
<div>Item1</div>
<div>Item2</div>
<div>Item3</div>
</div>
</div>
</body>
素のCSSとの対応は以下のようになります。
項目 | 素のCSS | TailwindCSS(ホバーで表示する場合) |
---|---|---|
親要素 | .dropdown | .group |
子要素 | .menu | .group-hover:visible |
ここまでのものを CodePen に置きました。
ただし、CodePen の仕様上 TailwindCSS はデフォルトのものをリンクすることしかできません。
そのため、生成されたものと等価なものを CodePen の CSS に貼り付けています。
サブメニューへの対応ですが、基本的には今までメニューを構築したものと同じになります。
今まで試作していたものに対して、メニュー部分全てをコピーし Item2
の要素と置き換えます。
インデントを調整すると以下のようになります。
ハイライトされた部分が、置き換わったところです。
<body>
<div class="relative group bg-red-100">
<div>
Menu
</div>
<div class="absolute invisible group-hover:visible bg-green-100">
<div>Item1</div>
<div class="relative group bg-red-100"> <div> Menu </div> <div class="absolute invisible group-hover:visible bg-green-100"> <div>Item1</div> <div>Item2</div> <div>Item3</div> </div> </div> <div>Item3</div>
</div>
</div>
</body>
ただし、これだけでは以下の問題があります。
これらの問題を解決します。
原因は、メニューに指定している group
やgroup-hover
を、サブメニューにも指定しているところにあります。
この問題を解決するため、これらを別のクラス名にして区別するようにします。
そのため、サブメニューの group
を group2
に変更します。
現在は group
と group2
だけですが、実際にはサブメニューの階層分必要になります。
残念なことに、現時点でのTailwindCSS(Ver.1.2.0)には group-hover
に対して別名をつけるというこの機能はありません。
そのため、プラグインを自作することになります。
今回は原理試作なので、CSSに以下のクラスを置いて対応することにします。
group2
を使用した記述は、以下のようになります。
.group2:hover .group2-hover\:visible {
visibility: visible;
}
これに従って、対応するHTMLには以下の記述に変更します。
<div class="relative group2 bg-red-100"> <div>
Menu
</div>
<div class="absolute invisible group2-hover:visible bg-green-100"> :
一般的なサブメニューでは、アイテムはホバーされている要素の下ではなく右または左に表示されます。
現状は何も指定していないので、下に表示されています。
これを、現在のメニューの幅だけ右側に表示するようにします。
サブメニューの移動量を決めるために、メニューの幅を指定するように変更します。
今回は、メニューの幅を .w-20
とします。
つまり、サブメニューは .w-20
分だけを右に移動する必要があります。
これは、TailwindCSSの記述ルールでいくと .left-20
と指定することが想定される場面になります。
ところが、.left-20
は TailwindCSS には存在しません。
そのため、これもプラグインで作り出すことになります。
また、高さ方向はホバーされている要素と合わせるため .top-0
を指定します。
これらを踏まえると、指定は以下のようになります。
<div class="absolute w-20 top-0 left-20 invisible group2-hover:visible bg-green-100">
:
原理試作の段階では、CSSに .left-20
を別途定義することで対応します。
20に割り当てられている数値ですが、本家のサイトの以下の部分に記載されています。
https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale
ということで、left-20
は以下のようになります。
.left-20 {
left: 5rem;
}
ここまでのものを CodePen に置きました。
目的のクラスを生成するために、プラグインを作成します。
プラグインの作成については、以下を参考にしました。
https://tailwindcss.com/docs/plugins
デフォルトの group-hover
を生成しているソースコードが以下にあります。
https://github.com/tailwindcss/tailwindcss/blob/master/src/lib/substituteVariantsAtRules.js
これを参考に、若干の仕様変更を入れながらプラグインを実装します。
関数内で使用しているモジュールで必要なものをインストールします。
$ npm i -D postcss-selector-parser
関数のパラメータは以下のようにします。
パラメータ名 | 内容 |
---|---|
hoverName | group ,group-hover の group 部分に相当する文字列を指定 |
combinator | 2つのセレクタの結合子を指定 |
結合子は以下のものに対応します。
結合子 | 記号 | 内容 |
---|---|---|
子孫結合子 | 空白文字(デフォルト) | 1つ目のセレクターに一致する要素の子孫のうち、2つ目のセレクターに一致する要素を選択 |
隣接兄弟結合子 | + | 同じ親要素の子同士であって、1つ目の要素の直後にある2つ目の要素を選択 |
一般兄弟結合子 | ~ | 同じ親要素の子同士であって、1つ目の要素の後にある2つ目の要素を選択 |
子結合子 | > | 2つ目のセレクターが1つ目のセレクターの子要素の場合にのみマッチ |
上記を踏まえ、プラグインの実装は以下のようになりました。
const plugin = require('tailwindcss/plugin');
const selectorParser = require('postcss-selector-parser');
const prefixSelector = require('tailwindcss/lib/util/prefixSelector.js').default;
function _addGroupHoverVariant(hoverName, combinator = " ") {
if(!combinator.match(/^[>+~ ]$/)) {
throw `Invalid combinator. Your argument '${combinator}' is not acceptable.`
}
combinator = combinator.replace(/([>+~])/," $1 ");
return plugin(function({ addVariant, config }) {
addVariant(`${hoverName}-hover`, ({ modifySelectors, separator }) => {
return modifySelectors(({ selector }) => {
return selectorParser(selectors => {
selectors.walkClasses(sel => {
sel.value = `${hoverName}-hover${separator}${sel.value}`;
sel.parent.insertBefore(sel, selectorParser().astSync(prefixSelector(config('prefix'), `.${hoverName}:hover${combinator}`))
)
})
}).processSync(selector)
})
})
});
}
指定するのが group
group2
だけの場合、使用方法は以下のようになります。
// tailwind.config.js
module.exports = {
theme: {
extend: {},
},
variants: {
visibility: ['responsive', 'hover', 'focus', 'active', 'group-hover', 'group2-hover']
},
plugins: [
_addGroupHoverVariant('group2'),
],
};
spacingの値は、Theme Configurationから取得できます。
以下を参照すると、theme.spacingに目的のものが入っています。
https://tailwindcss.com/docs/theme/#spacing
上記の各値に対してクラスを出力するようにします。
ということでmap関数を使用したいので、まずはlodashをインストールします。
npm i -D lodash
クラスの出力方法は以下が参考になります。 (2020.4.26追記) https://tailwindcss.com/docs/plugins#escaping-class-names
実装は以下のようになりました。
パラメータ名 | 内容 |
---|---|
atr_name | left , right 等を指定 |
const _ = require('lodash');
function _addSpacingUtility( atr_name ) {
return plugin(function({ addUtilities, e, config }) {
const newUtility = _.map(config('theme.spacing'), (value, key) => {
return {
[`.${e(`${atr_name}-${key}`)}`]: {
[`${atr_name}`]: `${value}`
}
}
});
addUtilities(newUtility);
});
}
使用方法は以下になります。
この例では、top
left
right
を指定しています。
// tailwind.config.js
module.exports = {
theme: {
extend: {},
},
variants: {
:
},
plugins: [
_addSpacingUtility('top'),
_addSpacingUtility('left'),
_addSpacingUtility('right'),
],
};
これで目的のものは全部作成しました。
「CSSのみで実現するドロップダウンメニューの作成」という記事はいろいろなところで見るのですが、TailwindCSSでの実現方法が記述されているもの、特に階層メニューの実現方法について言及されているものは、ほとんどありませんでした。
ということで、今回作ってみました。
TailwindCSSの内部構造により仲良くなれたので良かったと思います。
次回はPhoenixプロジェクトを作成し、今回の成果を使用してプログラマブルに階層メニューを実現する方法を紹介します。