🍣

[CSS組版]腑分けして理解したいCSS組版 ナビゲーション(柱稽古編)

2024/12/14に公開

CSS組版アドベントカレンダー2024の14日目の記事です。

https://adventar.org/calendars/10448

ナビゲーションについて書こうと思っていたんですが、2つに分けた方が書き易そうなのでそうします。

ナビゲーション(デカ主語)

ナビゲーションというのは日常語としては結構包含する範囲の広い言葉です。
ここでは、次のように絞って考えることにします。

  1. 現在地の情報を示すもの ex.「ここは地獄の一丁目です」
  2. 目的地の情報 ex.「この先3kmで地獄の一丁目」
  3. (案内方法)

3は置いておいて、ページメディアにおいて1の役割は何でしょうか。適当に本を開いてください(写真中心の雑誌とかだとダメかも)。

何ページですか、何のセクションですか? はい。今ヘッダ若しくはフッタを見ましたね?

ということで、ヘッダまたはフッタ(running header, footerまたは「柱」)[1]には、そのページの現在地としての情報が期待されます。

2の役割は1の情報を伝えるものとすれば、「目次」「索引」「相互参照」といったものが担うことになります。

1のコンテンツは、元の原稿データにはなく[2]、組版を行うときに付加されることが期待されます。2はちょっとややこしくて、本文コンテンツとして「ある地点を示している」情報はCSS適用前にあり、CSS組版後は「組版時に組版後の情報に置き換わること」が期待されます。idが書かれていたのがページ番号に置き換わると嬉しいですね。

そんなわけで、2の方は1ありきな所もあるので、今回は1、柱の話をします。

コンテンツ。そう、コンテンツです。CSSによって追加されるGenerated ContentがCSS組版のナビゲーション表示機能を担います。通常のWebページで::before::aftercontentプロパティに慣れ親しんでいれば殆ど追加で覚えることはありません。

https://www.w3.org/TR/css-gcpm-3/
https://www.w3.org/TR/css-content-3/

柱はページの余白に置かれる

やっぱりカラフルな雑誌だと分かり辛いので、新書や小説のページを見てもらいたいのですが、柱というのは本文と離れた位置にあります。本文と見分けが付く状態であることもナビゲーションとして大事なことです。

ページレイアウトの記事のときに、ページマージンを先に設定するのか本文領域を先に設定するのかという話をしました。そう、ページマージンの箇所は本文と被らない位置となります。@pageの中で幾つかのmargin at ruleによってページマージンの場所にコンテンツを配置可能です。これらをpage margin boxと呼びます。

margin at ruleの対応位置を示します。

@top-left-corner @top-left @top-center @top-right @top-right-corner
@left-middle 本文領域 本文領域 本文領域 @right-middle
@bottom-left-corner @bottom-left @bottom-center @bottom-right @bottom-right-corner

ドサっと書くだけだとアレなのでちょっと補足します。よく使われるのは次の6つです。

  • @top-left@top-center@top-right(ヘッダ用に)
  • @bottom-left@bottom-center@bottom-right(フッタ用に)

「左ページの下端にページ番号を置きたい」というとき@bottom-leftにその指定をする(後述)、といった具合です。

柱にコンテンツを置いてみる

マージンボックスへのコンテンツは、contentで指定します。当然、文字列やurl()による画像が配置できます。

@page {
  @bottom-center {
  content: "全ページフッタに表示";
  }
}

まあ、全ページに同じ内容が書いてあったらナビゲーションとしては微妙ですよね(シリーズの論文誌など、巻数情報を含めた文書全体の情報を全ページに載せる意義のある文書もありますが)。

柱にページ番号を置いてみる

Generated Content Moduleの管轄になりますが、CSSではユーザが独自のカウンタを設定できます。これをcounter-setcounter-resetcounter-incrementで制御しますが、GCPMでは制御を最初から良い感じにやってくれる定義済みカウンタとしてpageがあります。pageはページが変わるごとに1ページずつ増えていくカウンタです。

ということで、先程のテキストの代わりにページ番号カウンタを置きます。装飾がてら「~」のテキストも前後に置いてみます。

@page {
  @bottom-center {
 /*全ページフッタにページ番号を表示   「~20~」のように表示される */
  content: "~" counter(page) "~";
  font-variant: oldstyle-nums; /*折角なので数字表示をoldstyle-numsにする*/
  }
}

これでページが進むごとにページ番号が表示されるようになりました。マージンボックスルールではcontent以外もfont-sizeやらbackground-colorやらでスタイルもある程度調整可能です。

柱にそのセクションの見出し情報を置いてみる string()string-set

ページカウンタは勝手に進むものだからいいとして、「ページ内でどのセクションが何ページから何ページまで」って、組版するまで確定しない情報ですよね。「んー多分16ページから2章だから@pageをこれに切り換えて」などやっていられません。やらなくていいんですそんなこと。

  1. ページマージンボックスで柱に表示したい内容をcontent: string(<名前>);としておきます
  2. 柱に出したい構造のCSSにstrng-set: content(<名前>)のように指定します
  3. 組版実行!
@page {
  @top-center {
    content: string(chaptertitle);
}

main > section > h2 {
string-set: chaptertitle content(); /* content(text)でも同じ。h2内のテキストを名前付き文字列の内容にセットする */
}
<main>
  <section><h2>My Story</h2>...</section>
  <section><h2>Your Story</h2>...</section>
</main>

組版結果では、ページでMy Storyの見出しがあるセクションではヘッダ中央(@top-center)に「My Story」、Your Storyの見出しがあるセクションではヘッダ中央に「Your Story」が表示されることになります。

上の例でのchaptertitleのように、string-setで内容を指定され、string()で呼び出されるものを名前付き文字列と呼びます。ユーザが独自に指定可能です。

同じページ内でstring-setの名前付き文字列が被ったら?

章ならともかく、節などでは同ページ内で複数のセクションが存在することもあります。
実はstring()は、ページ内の登場位置によって採用する優先順を変える追加の引数を指定可能です。(無指定の場合初期値first

  • first ページ内で開始される最初のもの
  • start ページ内で最初のもの
  • last ページ内で開始される最後のもの
  • first-except ページ内にある最初の領域のものを優先するが、空文字列は除く

firststartの違いですが、「ページの開始時点で使用可能な値があるとき」の挙動が異なります。https://www.w3.org/TR/css-gcpm-3/#example-23e3824c のfigure 3ですね。

左右ページで柱のコンテンツを変えよう

ここまでの内容を基に、左ページはヘッダの左端にページ番号で真ん中に章題、右ページはヘッダの右端のページ番号で真ん中に節題としましょう。

@page :verso{
  @top-left {
  content: counter(page);
  }
  @top-center {
    content: string(chapter);
   }
}

@page :recto{
    @top-right {
    content: counter(page);
   }
   @top-center {
    content: string(section);
   }
}

ページマージンボックス内の細かい位置は?

ページマージンボックス内の揃え位置は各ボックスによって初期値が良い感じになっています。@top-leftならtext-align:left@top-rightならtext-align:rightといった感じですね。

vertical-alignmiddleが初期位置の筈。

揃えではなかったり、ボックス間の配置関係の話となるとPaged Media仕様を見つつ頭を捻りながら確認することになります。作業の日数が無いのであれば諦めた方が良いかもしれません。

tips:章番号と章題 to string-set

章番号と章題って、まとめてしまうとちょっと微妙で、具体的はまとめてしまった章番号と章題間のスペースの調整は後からは困難になります。

後から章番号と章題間のスペースを調整できない指定
@page {
 @top-center {
  /* ここからスペースの調整などはできない */
  content: string(heading);
  }
}

h2 {
string-set: heading content(before) " " content(text);
}

この解決策は、別の名前付き文字列としてセットして別々に使うことです。

@page {
 @top-center {
  /*別の名前付き文字列をそれぞれ呼ぶ*/
  content: string(chapterno) "     " string(chaptertitle);
  }
}

h2 {
/* ","区切りで別々の名前にセット */
string-set: chapterno content(before),  chaptertitle content(text);
}

発展編 position:running()content:element()

string-setでガチャガチャやっているとふと思うでしょう。

「あれ、柱ってテキストとかカウンタだけ? もっと装飾できないの?」

要素をそのまま柱のコンテンツにする方法があります。それがposition:running()content:element()です。

@page ... {
/*要素そのまま配置したいマージンボックス*/
  @top-right {
  content: running(sectionelement);
  }
}
/*マージンボックスに配置したいコンテンツ*/
nav.forrunning {
position: running(sectionelement);
...
}
<section><h2>...複雑な見出し...</h2><nav class="forrunning"><div class="complecated">...<span>見出し</span></div></nav>
...
</section>

使い方としてはstring()string-set()のように、名前を付けた内容をマージンボックスのコンテンツとして反映するというものです。しかし、positionで指定していることに注意してください。「本文領域ではなくマージンボックスにコンテンツ表示位置を移動する」という意味付けになっていて、元の本文コンテンツとしては削除されたような状態になります。上の例でh2に指定しているのではなくnavを用意しているのはそのためです。h2position: element()を指定していたら、端的には見出しがふっとびます。

脚注
  1. 他にもツメ(Thunm index)なんかの場合もありますね ↩︎

  2. Webページを印刷したときにURLをブラウザが付けてくれたりしますがそういう話ではない ↩︎

組版・ドキュメンテーション勉強会

Discussion