JavaScriptを使わずにCSSグリッドで動くCSSオンリーのクロスワードパズルを作成したところ、CodePenで話題になりました。執筆時点でハートマークが350、ページビューは24,000を超えました。
チュートリアルCSS Grid Gardenに触発され、グリッドレイアウト機能でなにか作りたくなりました。クロスワードパズルの作成にうってつけの機能なので、JavaScriptをまったく使わずに完成させました。
グリッドの盤面を作成
盤面本体は以下の構造にしました。HTMLには、各セクションで実現することをコメントしました。
<div class="crossword-board-container">
<div class="crossword-board">
<!-- input elements go here. Uses CSS Grid as its layout -->
<div class="crossword-board crossword-board--highlight crossword-board--highlight--across">
<!-- highlights for valid 'across' answers go here. Uses CSS Grid as its layout -->
</div>
<div class="crossword-board crossword-board--highlight crossword-board--highlight-down">
<!-- highlights for valid 'down' answers go here. Uses CSS Grid as its layout -->
</div>
<div class="crossword-board crossword-board--labels">
<!-- row and column number labels go here. Uses CSS Grid as its layout -->
</div>
<div class="crossword-clues">
<dl class="crossword-clues__list crossword-clues__list--across">
<!-- clues for all the 'across' words go here -->
</dl>
<dl class="crossword-clues__list crossword-clues__list--down">
<!-- clues for all the 'down' words go here -->
</dl>
</div>
</div>
</div>
基本的な骨組みができたので、要素の追加とスタイリングに入ります。
Form要素でマスを作成
13×13のグリッドで、黒マスが44個のクロスワードパズルを作ります。これにはitem{row number}-{column number}の形式、つまりそれぞれがitem4-12の固有のIDをもつ125個のinput要素が必要です。
各input要素のminlengthとmaxlengthを「1」に設定してクロスワードパズルの反応(1マスにつき1文字)に対応します。各input要素にはrequired属性も設定し、HTML5のフォームバリデーションに対応します。HTML5属性すべてCSSで実装しました。
一般兄弟セレクタの使用
input要素は視覚的に(まさにクロスワードパズルの形に)グループ分けされます。input要素の各グループはクロスワードでの1単語を表します。グループ内の各要素が有効(正解)なら(疑似セレクタ:validで確認可能)、DOMの下位に出現する要素をCSSで(一般兄弟セレクタと呼ばれる高度なCSSセレクタで)スタイリングして、単語が正しいと表示します。
兄弟セレクタとCSSの動作から、この要素はDOMの後ろに配置します。CSSでスタイリングできるのは、現在選択されている要素より後ろの要素です。DOMをさかのぼる(DOMツリーの上の階層にいく)ことや、現在の要素より前の部分をスタイリングすることは、少なくともいまのところはできません。
有効な要素のスタイリングには疑似クラス:validを使います。
.input:valid {
border: 2px solid green;
}
.input:invalid {
border: 2px solid red;
}
DOMの後ろに出現する別の兄弟要素のスタイリングには、「〜(チルダ:一般兄弟)セレクタ」を使って、A ~ Bと記述します。このセレクタは、Aの兄弟であり、DOM内でAのあとに出現する要素で、Bにマッチするすべての要素を選択します。例を示します。
#input1:valid ~ #input2:valid ~ #input3:valid ~ #input4:valid ~ #input5:valid ~ .valid-message {
display: block;
}
input要素がすべて有効な場合valid-message要素を表示します。
ここでは一般兄弟セレクタが便利です。クロスワードを動かすには、確実にすべての要素で一般兄弟セレクタを使えるようにレイアウトすることが必要です。
完成したクロスワードのサンプルには上のテクニックを使っています。285行目のコードを抽出しました。
#item1-1:valid ~ #item1-2:valid ~ #item1-3:valid ~
#item1-4:valid ~ #item1-5:valid ~ #item1-6:valid ~
.crossword-board--highlight .crossword-board__item-highlight--across-1 {
opacity: 1;
}
このCSSですべてのinput要素が有効と確認すると、.crossword-board__item-highlight--across-1要素の透明度が変化します。.crossword-board--highlightはinput要素の兄弟要素で、さらに.crossword-board__item-highlight--across-1は.crossword-board--highlightの子要素です。CSSで選択できるのです。
正解判定表示
クロスワードの「答え」、つまりinput要素のグループには、対応する「正解判定表示」のグリッドアイテム(.crossword-board__item-highlight--across-{{clue number}})を設定します。グリッドアイテムはz軸でinput要素の奥に配置し、opacity: 0に設定して非表示にします。正しい単語を入力すると正解判定表示グリッドアイテムのopacityが1になり、疑似クラスセレクタのスニペットに示す正解判定を表示します。
input要素の「単語」グループごと繰り返します。単語グループのinput要素用にそれぞれCSSルールを手動で作成し、対応する正解判定表示グリッドアイテムを選択するのですが、想像通りCSSがあっという間に肥大化します。
従って、ヨコのカギの答えすべてに対する正解判定の表示・非表示を切り替えるCSSルールは論理的手法で作成します。タテのカギの答えも同様です。
グリッドシステムにおける課題
CSSの軽量化は、明示的な宣言をしなければ同一のグリッドシステム内でグリッド領域をオーバーラップできません。グリッド領域は互いに隣接して配置します(「ヨコに1マス進み、1マス下がる」場合、盤面で右上のマスを共有します。単一のCSSグリッドですべての正解判定表示をレイアウトするのは無理なのです)。
解決策は、ヨコの正解判定表示グリッドアイテムはグリッドシステム内でラップし、タテの正解判定表示グリッドアイテムは別に設定します。これで互いに干渉したりグリッドレイアウトが崩れたりすることなく、一般兄弟セレクタでCSSを使ってグリッドアイテムを選択できます。
CSSグリッドアイテムの挙動はインラインブロック要素と同様です。同一のスペースに2つのグリッドアイテムを指定すると、グリッド内では2番目のアイテムが回り込んで1番目のアイテムのあとに表示されます。
上の例で、最初のグリッドアイテムは1カラム目から7カラム目まで7カラム分を占めます。2番目のグリッドアイテムは4カラム目から9カラム目まで占めます。CSSグリッドは、同じ行内では対応できないため、2番目のグリッドアイテムは次の行に回り込みます。2番目のアイテムの行をgrid-row: 1/1に設定すると、優先されて1番目のグリッドアイテムが2行目に移動します。
この状況を回避するためヨコとタテのアイテム用に複数のグリッドを用意しました。各要素用に行と列のスパンを指定して回避する方法もありますが、CSSの分量を減らし、HTML構造のメンテナンス性を高めるために上の方法を使っています。
入力された文字が合っているかチェック
各input要素にはpattern属性が記述され、値を正規表現で設定しています。正規表現はマスに設定された文字に大文字・小文字の区別なくマッチします。
<input id="item1-1" class="crossword-board__item"
type="text" minlength="1" maxlength="1"
pattern="^[sS]{1}$" required value="">
これでは答えがHTMLに入るので理想的とは言えません。答えはCSS内にとどめたいので、次のテクニックを試しました。
.input#item1-1[value="s"],
.input#item1-1[value="S"] {
/* do something... */
}
これではうまく動きません。HTML内に存在する属性セレクタに基づいて要素を選択し、動的な変更には反応しないため、疑似クラス:validに頼らなければならず、答えがHTML本体に入ってしまいました。
ホバー時にカギをハイライトする
タテヨコのカギはどちらもdiv内にラッピングしています。ラッピングdiv要素は、クロスワードのグリッドにおけるinput要素の兄弟要素で、先ほどのコードブロックのHTML構造に示されています。フォーカス(ホバー)しているinput要素に従って適切なカギ(1つの場合も、複数の場合もある)を簡単に選択できます。
実装するにはinput要素に:active、:focus、:hoverをスタイリングします。ユーザーが要素にインタラクションしたら背景色を適用して、適切なカギをハイライトします。
#item1-1:active ~ .crossword-clues .crossword-clues__list-item--across-1,
#item1-1:focus ~ .crossword-clues .crossword-clues__list-item--across-1,
#item1-1:hover ~ .crossword-clues .crossword-clues__list-item--across-1 {
background: #ffff74;
}
カギに番号を振る
カギの番号はCSSグリッドパターンで配置します。
<div class="crossword-board crossword-board--labels">
<span id="label-1" class="crossword-board__item-label crossword-board__item-label--1">
<span class="crossword-board__item-label-text">1</span></span>
<span id="label-2" class="crossword-board__item-label crossword-board__item-label--2">
<span class="crossword-board__item-label-text">2</span></span>
<!-- rest of the items here..... -->
</div>
CSSは以下の通りです。
.crossword-board__item-label--1 {
grid-column: 1/1;
}
.crossword-board__item-label--2 {
grid-column: 4/4;
}
/* etc... more items here... */
各番号を関連するinput要素のグループの開始位置に配置します。番号の幅と高さは、グリッド1マス内のできるだけ小さなスペースに収まるように設定します。別の方法でCSSグリッドを実装すればスペースを小さくできますが、あえてこの方法にしました。
「正解マスを確認」のチェックボックス
クロスワード画面の上部に「正解マスを確認(Check for valid squares)」というチェックボックスがあります。ユーザーは単語が間違っていても特定の文字が合っているか確認できます。
単一のCSSルールで正解のマスをすべてハイライトできるので、チェックボックスの実装は大きなメリットがあります。the checkbox hackを使って、DOMでチェックが入ると正解のマスをすべて選択します。
#checkvaliditems:checked ~ .crossword-board-container .crossword-board__item:valid {
background: #9aff67;
}
最後に
デモで使った主要なテクニックは以上です。この試作は近年のCSSの発展を示しています。工夫して使える機能はたくさんあります。いろいろな新機能を試し、どこまでできるのか挑戦したいです。
紹介したCSSを試したいならCodePenのコレクションに全体のサンプルコードがあります。完成版CSSクロスワードパズルの動作はこちらで確認できます。
(原文:How I Built a Pure CSS Crossword Puzzle)
[翻訳:新岡祐佳子/編集:Livit]