【Hexo】理想的なDetails/Summaryタグ【アコーディオンメニュー】

【Hexo】理想的なDetails/Summaryタグ【アコーディオンメニュー】

Intro

長い説明と記事の見栄えを両立したい……detailsで折りたたもう!

TL; DR

details/summaryタグを使ったアコーディオンメニューをHexoの記事に導入する方法を説明する。
さらに、アコーディオンメニューの見た目を整えるためにCSSとjsをカスタマイズし、summaryを追従させる方法を説明する。

アコーディオンメニュー

まず、節の名前にもあるアコーディオンメニューとは何だろうか。

アコーディオンメニューは、Webページ上で複数の項目を折りたたみ表示するUIパターンのことである。ユーザーが項目をクリックすると、その項目の詳細が表示される仕組みである。

必要な説明や詳細ではあるが、記事全体のバランスから常には表示しておくのを避けたい場合に便利である。

これはHTMLの場合detailsとsummaryのタグによって実現される。

詳細

展開された本文

上の例では、<details>タグで囲まれた<summary>タグが押されると、その下にある<p>タグが表示される。

1
2
3
4
<details>
<summary>詳細</summary>
<p>展開された本文</p>
</details>

不満点

機能的には、この通りデフォルトで備わっている内容で十分なのだ。ワンクリックで開閉できる。

htmlタグでの記入が面倒

まず、一つ目の不満点は、htmlタグでの記入が面倒であることだ。
先ほどの例では、<details>タグと<summary>タグを使っている。正直あまり長い記入量ではないが、入力の手間が切り詰められたMarkdownにおいては十分面倒に映る

これは簡単な変換で完了できるため、すでにHexo Plugin「hexo-tag-details」が公開されていたので、それを利用させてもらうことにした。

Warning

後で述べるが、最終的にはこのPluginを少しカスタマイズして利用している

利用方法はリンク先の通りであり、シンプルだ。
Hexoで広く利用されているテンプレートタグ{\% ... %}を用いて表記する
先のアコーディオンの例を再現すると、下記の通りだ。

1
2
3
{% details 詳細 %}
<p>展開された本文</p>
{% enddetails %}

Tweet

このテンプレートタグ{\% ... %}の入力も正直言うと面倒……。GitHub Copilotなしでは無理
Snippetsの不具合解消をまじめに取り組むべきなのかも。

テンプレートタグ入力を助けてくれるGitHub Copilotくん

見た目が古臭い

既に見た通り、デフォルトのアコーディオンでも最低限の機能は持っているが、見た目がダサいのだ。テキストで表現しました感が強すぎる

ちなみに、このページのアコーディオンは、すでにもろもろの改善が施された後なので、そのつもりで……。
参考までに、下記がなんの変更も加えていないデフォルトのアコーディオンの見た目だ。
今となっては非常に物足りない……。

pure アコーディオンメニュー

CSSやStylusの説明 by ChatGPT

では、実際にアコーディオンの見た目を整える前に、CSSやStylusについてChatGPTの説明を載せておこう。

CSSやStylusの説明 by ChatGPT

CSS(Cascading Style Sheets)について

CSS(Cascading Style Sheets)は、ウェブページの見た目やレイアウトを指定するための言語である。HTML(HyperText Markup Language)はウェブページの構造を定義するが、CSSはそのデザインやスタイルを決定する役割を持っている。

例えば、以下のようにHTMLファイルがあるとする。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>サンプルページ</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="container">
<h1>こんにちは、世界!</h1>
<p class="intro">これはサンプルページです。</p>
</div>
</body>
</html>

このHTMLにCSSを適用することで、見た目を変更できる。以下はstyles.cssファイルの例である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.container {
width: 80%;
margin: 0 auto;
}

.container h1 {
color: blue;
text-align: center;
}

.container .intro {
font-size: 16px;
color: green;
}

このCSSを適用すると、.containerクラスを持つ<div>内の<h1>タグのテキストは青色になり、中央に配置される。.introクラスを持つ<p>タグのテキストは緑色で、フォントサイズが16pxになる。

直接要素にstyle属性を持たせる

HTMLの各要素に直接style属性を使ってスタイルを指定することもできる。例えば、上記のHTMLに直接スタイルを適用する場合は以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>サンプルページ</title>
</head>
<body>
<div class="container" style="width: 80%; margin: 0 auto;">
<h1 style="color: blue; text-align: center;">こんにちは、世界!</h1>
<p class="intro" style="font-size: 16px; color: green;">これはサンプルページです。</p>
</div>
</body>
</html>

直接style属性を使うデメリット

  • 再利用性が低い:異なる場所で同じスタイルを適用するために、何度も同じスタイルを記述する必要がある。
  • メンテナンスが難しい:スタイルを変更する場合、複数の場所で同じスタイルを更新しなければならない。
  • 可読性が低下:HTMLとCSSが混在するため、コードの可読性が低下し、管理が難しくなる。

Stylusについて

Stylusは、CSSをより簡潔かつ効率的に記述するためのスタイルシート言語である。CSSのプリプロセッサの一つであり、変数やネスト、ミックスインなどの機能を提供する。

例えば、以下のようなCSSをStylusで書くと、以下のようにより簡潔に記述できる。

1
2
3
4
5
6
7
8
9
10
11
.container
width 80%
margin 0 auto

h1
color blue
text-align center

.intro
font-size 16px
color green

Stylusはインデントベースの構文を採用しており、中括弧やセミコロンが不要であるため、コードが見やすくなる。

SCSSについて

SCSS(Sassy CSS)は、Sass(Syntactically Awesome Style Sheets)の一つのバリエーションであり、CSSに拡張機能を追加したプリプロセッサである。SCSSはCSSと互換性があり、既存のCSSコードをそのまま利用できるのが特徴である。

例えば、以下のようなCSSをSCSSで書くと、以下のようにネストや変数を利用して記述できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$primary-color: blue;
$secondary-color: green;

.container {
width: 80%;
margin: 0 auto;

h1 {
color: $primary-color;
text-align: center;
}

.intro {
font-size: 16px;
color: $secondary-color;
}
}

SCSSでは変数やネスト、ミックスイン、継承などの機能を提供し、再利用性やメンテナンス性を向上させることができる。

まとめ

CSSはウェブページの見た目やレイアウトを指定するための基本的な言語である。直接style属性を使用してスタイルを指定することもできるが、再利用性やメンテナンス性の観点からはあまり推奨されない。StylusやSCSSは、CSSをより効率的に記述するためのプリプロセッサで、それぞれ独自の強力な機能を提供する。Stylusはインデントベースの構文で簡潔に書けるのが特徴で、SCSSはCSS互換性を保ちながら拡張機能を追加するのが特徴である。どちらを使うかはプロジェクトやチームのニーズに応じて選ぶと良い。

見た目のカスタマイズ

アコーディオンの見た目の改善はみなが感じることのようで、調べればたくさんの記事が見つかる。

Tweet

これは、厳密には正しい表現ではない。HTMLのデフォルトのアコーディオンメニューが機能的にはそれほど不満でもないため、独自実装ではなく見た目の調整から始める人が多いのだ。

実際に参考にしたサイトは下記の通りだ。
その結果得られたarticle.stylへの追記内容は次の節に記載している。

  • details/summaryタグ - catnose
    • アコーディオンメニューの三角形の見た目、開閉時のアニメーション、hover時のスタイルなど多くを参考にした

summaryの追従

最低限の見た目を整えたところで気が付いた。
アコーディオンメニューが長いと、summaryが画面外に行ってしまうことがある。
利用体験を考えると、summaryが画面内に留まると嬉しいよなぁ

sticky

実はこれは単純で、styleとしてposition: stickyを指定すれば良い。
ついでに、後ろの内容と重複するとsummary部分が読みづらいので、背景にぼかし効果を入れておく。

article.styl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
article
$.article
.content
details
summary
position: relative
display: block
cursor: pointer
padding: 1em 20px
margin: 1em
position: sticky;
top: 0.5em;
background : rgba(255,255,255,0.3)
backdrop-filter: blur(12px);
z-index : 2
transition: 0.2s;

&::-webkit-details-marker
display: none;


&:hover
cursor: pointer /* カーソルを指マークに */
background: rgba(235,235,235,0.3)
backdrop-filter: blur(12px);
z-index : 2

&:before, &:after
content: "";
margin: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
&:before
width: 16px;
height: 16px;
border-radius: 4px;
background-color: #1da1ff;
animation: rotateClose 0.2s ease-in;
&:after
left: 6px;
width: 5px;
height: 5px;
border: 4px solid transparent;
border-left: 5px solid #fff;
box-sizing: border-box;
transition: .1s;

/* オープン時 */
details[open]
summary
&:before
animation: rotateOpen 0.2s ease-in forwards;

&:after
transform: rotate(90deg); /* 90度回転 */
left: 4px;
top: 5px;

&.is-opened summary::after
transform:translateY(-50%) rotate(90deg);


article.is-primary
overflow: hidden;
opacity: 1;
visibility: visible;
transition: padding .25s, height .25s, opacity .25s;


@keyframes close
0%
opacity: 1;

100%
opacity: 0;


@keyframes rotateClose
0%
transform: rotate(90deg);

100%
transform: rotate(0deg);



@keyframes open
0%
opacity :0;
transform: translateY(1em);

100%
opcaity: 1;
transform: none;


@keyframes rotateOpen
0%
transform: rotate(0deg);

100%
transform: rotate(90deg);

追従状態での開閉

さて、summaryが追従するようになったことによるうれしい誤算として、長いアコーディオンメニューを読んでいる途中でも、summaryをクリックして開閉できるようになった。
もともと長いアコーディオンメニューを閉じたい場合は、summaryが出てくるまで上へスクロールする必要があったが、これでその必要がなくなったわけだ。

長いアコーディオンこそ途中で閉じたくなることを思うと、これはなかなかよい改善だったのではないだろうか。

ただ、これで新しい問題が発生した。

不満点: 飛ばされる

長いアコーディオンメニューを閉じた場合、突然そのアコーディオンメニューの高さが一行分になる。
画面上部からのスクロール量は保持されるので、いきなり画面下部へと飛ばされてしまうわけだ。

メニューを閉じただけで、まだ読んでいない記事後半に飛ばされるのは気持ちのいいものではない。
単にネタバレというだけではなく、いちいちスクロールで戻る必要がある。
そうなると、ユーザーはsummary追従状態でアコーディオンを閉じることはなくなるだろう。

対策として、閉じる直前の状態に合わせて追加スクロールを実施することにした。
具体的には下記の3通りだ。

その際気を付けるべき点として「読者が期待する表示内容が保持されること」がある。
すなわち、アコーディオンメニューを閉じたあとに、読者は次に読みたい内容を保持できているか。アコーディオンメニューの前後の内容が表示されているかどうか。記事の前半や後半に飛ばされるのはもってのほかだ。

さらに、読者が不要に視線を動かすことにならないかも重要だ。
詳しくは以降で語ろう。

Tweet

この先の内容の説明の画像は、また後程追加する

A. アコーディオン上端が画面内に存在する場合: 何もしない

アコーディオンメニューが画面内に存在する場合、追加スクロールは不要である。

  • 視線移動の不要性
    • アコーディオンメニュー直前の内容の高さは変化しない。そのため、画面内の要素の高さは不要に変更されていない。

B. not Aかつ、アコーディオンメニュー下端が画面内に存在する場合: アコーディオンメニュー下端の高さを保持するようにスクロール

追加スクロールを行わない場合、相当下の方に移動させられることになる。

小節名にもある通り、アコーディオンメニュー下端の高さを保持するようにスクロールすることで下記の通り解決する。

  • 視線移動の不要性
    • アコーディオンメニュー直後の内容の高さは変化しない。そのため、画面内の要素の高さは不要に変更されていない。
    • アコーディオンメニュー直前の内容はもともと画面内にないので気にする必要はない。

C. not Aかつnot B、すなわちアコーディオンメニューのみで画面が占められている場合: アコーディオンメニュー上端が画面上部からほどほどの高さに来るようにスクロール

この場合はアコーディオンメニューの隣の内容が画面外部の状態から開始するので、極論を言えば、画面内に閉じたアコーディオンメニューが表示されていれば、どの高さにスクロールしても問題はない。ただ、下記の理由から小節名にある判断をした。

  • 読者はアコーディオンメニュー以降の内容を読み進める見込みが高い
    • よって閉じた後のアコーディオンメニュー (summary) は画面上半分にあるとよい
  • 読者が次に読む内容の目印はsummaryになる
    • よって、summaryが画面端ぎりぎりにあるのは望ましくない

実装

上記の3つのケースに対応するため、下記のように実装した。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// details要素をsummaryで閉じたとき、閲覧内容への影響を抑えるように追加スクロール
const section = document.querySelector('section');
document.querySelectorAll('details').forEach(function (details) {
const summary = details.querySelector('summary');
summary.addEventListener('click', function (event) {
const windowHeight = window.innerHeight;
const detailsArticle = summary.nextElementSibling;

if (details.open) {
if (!detailsArticle) return;
if (detailsArticle.getBoundingClientRect().top > 0) {
;
} else if (detailsArticle.getBoundingClientRect().bottom <= windowHeight) {
setTimeout(() => {
window.scrollTo({
top: window.scrollY - detailsArticle.scrollHeight,
behavior: 'smooth'
});
}, 0);
} else {
setTimeout(() => {
window.scrollTo({
top: window.scrollY + details.getBoundingClientRect().top - 10,
behavior: 'smooth'
});
}, 0);
}
}
});
});

まとめ

アコーディオンメニューは、Webページ上で複数の項目を折りたたみ表示するUIパターンのことである。ユーザーが項目をクリックすると、その項目の詳細が表示される仕組みである。
今回、Hexoの記事にアコーディオンメニューを導入するために、Hexo Plugin「hexo-tag-details」を利用した。これにより、アコーディオンメニューの記述が簡単になった。
さらに、アコーディオンメニューの見た目を整えるために、CSSをカスタマイズした。また、アコーディオンメニューのsummaryを追従させることで、ユーザーがアコーディオンメニューを開閉しやすくした。

コメント