株式会社ディジタルグロースアカデミア リブロワークス部デザイン室の峠坂あかりと松澤維恋です。
リブロワークスでは、すでにいくつかの商業出版物でVivliostyleによるCSS組版を採用しています。実際の業務の流れとしては、リブロワークス デザイン室にて紙面デザインとCSSの作成を行い、それを社内外の編集者や著者に提供するという分業体制を敷いています。そのため、なるべく双方ともに作業しやすいような環境づくりを目指しています。
2023年に
円滑にCSS組版案件を進行するために
IT書の編集者、著者であっても、組版用のCSSを書ける人はそう多くはありません。初めてCSS組版の案件を進行する場合、
Markdown記法と表示結果をまとめる
円滑に進めるためには、

上記のようにスプレッドシートで見える化しておくことで、一目でMarkdownとデザインの対応を理解できます。また、複数人で作業する場合に、仕様書として利用することもできます。
CSSファイルを適切に分割する
使用するCSSファイルの数に制限はないため、あとからメンテナンスしやすいように、役割ごとに複数のCSSファイルを作って作業します。一例として、リブロワークスでCSSを作成する場合、次のような役割別にCSSを分割しています。
CSS | 役割 |
---|---|
main. |
基本的な要素 |
custom. |
その本にしかない要素。ミニコラムや側注など |
page_ |
CSS変数を集めている。@pageで設定するものが中心 |
font. |
フォントまとめ |
main-chapter1. |
章カラーなど、章ごとで異なる要素 |
表のように掲載される要素や状況に応じてCSSを使い分けることで、触れるべきCSSを瞬時に判断することができ、円滑に作業をするための仕組みを作ることができるのです。
CSSファイルの同期
CSSを読み込むためには、Markdown側にFrontmatterを記載します。
---
link:
- rel: 'stylesheet'
href: 'css/main.css' <!-- 読み込みたいCSS -- >
<!-- 複数のCSSを読み込む場合(ここから) -- >
- rel: 'stylesheet'
href: 'page_settings.css'
<!-- 複数のCSSを読み込む場合(ここまで) -- >
lang: 'ja'
---
Frontmatterはルールが厳しく、前後には半角の-
が必ず3つ、:
の後には必ず半角スペースが1つ、rel:
とhref:
の行頭を揃えるなど、守らなければならないルール存在します。しかし、コピー&ペーストなどを駆使して仕様どおりに記載すれば問題が起きることはまずありません。
また、CSS同士を紐付けたい場合は、CSSファイルに@import " ";
と記載することで、複数のCSSを読み込むことが可能です。例えば、main.
にpage_
を紐付けたい場合、main.
に以下のコードを記載します。
@import "page_settings.css";
---
link:
- rel: 'stylesheet'
href: 'css/main.css' <!-- page_settings.cssと紐付いたmain.css -- >
lang: 'ja'
---
CSS同士を紐づけることで、Frontmatter上にCSSファイルの指定をする必要がなくなり、Markdown側の記載をシンプルにすることができます。Markdownには、なるべく原稿のみを記載するようにしたいので、リブロワークスではCSS同士を紐付けておくことが多いです。
途中でデザインを変更したくなったら
CSS組版では、文字サイズや各パーツのサイズを先に決めておくことで、パーツ自体のデザインは後から丸ごと差し替えることが可能です。デザインが確定しないと作業できない従来の組版に比べ、デザインの差し替えが容易だったり、リアルタイムで反映できたりするのがCSS組版のメリットと言っても過言ではないので、デザインが未確定の案件での使用にも適していると言えます。


見出し付きの囲み記事を作る
ここからは、いよいよ実作業に関するノウハウの説明をしていきます。CSS組版をする上で、まず意識すべきポイントは、section要素です。標準的なMarkdownでは、見出しにするテキストの前に#
~######
を付けると、h1~h6要素に変換されます。Vivliostyleではこれに加えて、VFM
- 見出し+その下の要素をsection要素で囲む
- {.クラス名}を書いてclassを付与する
- 対になる
#
を書くことでsection要素の範囲(終わり位置) を指定できる
最後に記載した範囲の指定方法については、開発者コミュニティに提案して、検討のうえ追加された仕様です。section要素の範囲を明確にすることで、構造をわかりやすくし、デザインの適用範囲なども明確になります。
では、上記のsection要素についての説明をふまえ、まずは見出し付きの囲み記事を作る方法から解説します。ここで紹介する見出し付きの囲み記事とは、見出し+その下の要素で構成された記事全体を罫線や背景色で囲んだデザインを指します。
##### タイトルタイトルタイトル{.column}
コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文
#####
<section class="level5" aria-labelledby="タイトルタイトルタイトル">
<h5 class="column" id="h5_1">タイトルタイトルタイトル</h5>
<p>コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文</p>
</section>

紹介例のように、見出し+その下の要素で構成された記事全体を指定するためには、section要素に対してborderやbackgroundを設定する必要があります。h1~h6の親として自動生成されたsection要素を選ぶには、has()
を利用しましょう。has()
では、その意味のとおりcolumn
クラスの要素を直下に含むsection
」
section:has(> .column){ /* .columnを直下に含むsection */
border: solid 0.2mm #8df; /* 囲み罫線の設定 */
border-radius: 10px; /* 角丸設定 */
padding-inline: 6mm; /* 囲み内左右マージン*/
padding-block: 4mm; /* 囲み内上下マージン */
margin-block-start: 6mm; /* 囲み上マージン */
width: 114mm; /* 囲み幅 */
background: #eef; /* 囲み背景色の設定 */
}
気をつけるべきポイント
書籍によっては、h4
レベルの小見出しをh5
レベルの囲みに入れたい場合もあるかと思います。しかし、section要素が振られたの囲みに同ランク以上の要素を入れると、sectionクラスの入れ子構造が壊れてしまう可能性が高くなります。構造が壊れてしまうと、デザイン上の囲み範囲や、連番counter()
)
##### タイトルタイトルタイトル{.column}
コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文。
##### コラム内見出し
コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文コラムやメモなどの囲み記事本文。
#####

before、afterで飾りをつける
節見出しなどに飾りをつける場合に、頻繁に用いられるのが疑似要素の::before
、疑似要素の::after
です。疑似要素を絶対配置position: aboslute;
)::before
。要素の直後や下に置きたいものは::after
を使用しましょう。
section:has(> .column)::before { /* .columnを直下に含むsectionの擬似要素 */
content: "Column"; /* テキスト"Column" */
background-color: #8df;
color: #fff;
line-height: 5mm;
inline-size: 17mm; /* 擬似要素の横サイズ */
block-size: 5mm; /* 擬似要素の縦サイズ */
position: absolute; /* 絶対配置 */
border-radius: 10px;
text-align: center;
margin-block-start: -6.5mm;
}

/* 節番号 */
h2::before {
content: counter(chapter-counter) "-" counter(section-counter); /* counter関数(章番号)-counter関数(節番号) */
color: #585656;
counter-increment: section-counter; /* counter関数の変数名(節番号) */
font-size: 44Q;
line-height: 14mm;
text-align: center;
color: #fff;
padding-block-start: 27mm;
padding-inline: 0% 96%;
/* 背景画像 */
background-image: url("img/section-header.png"); /* 背景画像 */
background-size: var(--page-width); /* 背景画像サイズ */
background-repeat: no-repeat; /* 背景画像の繰り返し */
inline-size: var(--page-width); /* 擬似要素の横サイズ */
block-size: 79mm; /* 擬似要素の縦サイズ */
position: absolute; /* 絶対配置 */
inset-inline-start: -21mm; /* 左右位置指定 */
inset-block-start: -18mm; /* 上下位置指定 */
}

リード文を入れる
リード文とは、節タイトルの下に入れる導入文のことを指します。主に、内容に興味を持たせる文章や節全体の要約などが入りますが、シンプルなデザインなら本文の体裁をそのまま使うこともあるパーツです。<div class="section-lead">
と</div>
で囲み、位置調整を行いましょう。節見出しなど、他の要素と直結する要素なので、絶対配置position: absolute;
)
<div class="section-lead">
ここでは、Rubyのプログラムを作成して、実行するまでの流れを解説します。最初にエディターでプログラムを書いて、コマンドプロンプトを実行するだけで、すぐに結果を確認できます。
</div>
.section-lead {
position: absolute; /* 絶対配置 */
inset-inline-start: 12mm; /* 左右位置指定 */
inset-block-start: 41mm; /* 上下位置指定 */
inline-size: 36em; /* 横サイズ(文字数指定) */
line-height: 20Q;
}

ページに柱とノンブルを入れる
柱やノンブルなど、版面周囲の決められた場所におく要素には、ページマージンボックスを使用しましょう。ページマージンボックスは、下図のように版面の周囲16箇所で分けられているため、ノンブルや柱など、該当する範囲にそれぞれ設定することができます。@page
内にそれぞれの指定を書くことで反映されます。

@page :right {
/* ノンブル */
@bottom-right { /* ノンブルを挿入するエリア */
content: counter(page); /* ノンブルの設定(ページのcounter関数) */
text-align: end;
}
/* 柱 */
@bottom-center { /* 柱を挿入するエリア */
content: string(chapter-number, first) " " string(chapter-title, first); /* section内最初の章番号 section内最初の章タイトル */
}
}
h1 {
/* 柱のテキスト */
string-set: chapter-title content(), chapter-number content(before); /* h1のテキストをchapter-titleに設定 h1のbefore要素(章番号)をchapter-numberに設定 */
}

ページの端にツメを付ける
ツメrunning-elemnt
を使用します。running-elemnt
は、ドキュメント内の要素をページマージンボックスに表示できるCSS組版独自の機能です。
本記事の例では、sideindex
にposition: running(sideindex);
を設定し、ページマージンボックス側にcontent: element(sideindex);
を設定しています。章ごとに位置が変動するようにしたいので、top1
、top2
、top3
など位置に応じたクラスを設けたうえで、変動させたい値を記述しています。今回は章ごとに変動する例を記載していますが、節ごとにツメを動かすこともできるので、自由度ははるかに高いです。
ただし、ページマージンボックスのstring-set、stringのような自動で見出しのテキストを拾う機能がないので、テキストは自分で入力する必要があります。
<div class="sideindex top1>
コンピュータの重箱の隅
</div>
:root {
--sideindex-size: 9mm; /* ツメのサイズ */
}
@page :right {
@right-top {
content: element(sideindex);
}
}
.sideindex {
position: running(sideindex);
writing-mode: vertical-rl; /* 縦書き */
font-size: 12q;
}
/* 章ごとにツメ位置を変える */
.top1 {
margin-inline-start: calc(var(--sideindex-size) * 0);
}
.top2 {
margin-inline-start: calc(var(--sideindex-size) * 1);
}
.top3 {
margin-inline-start: calc(var(--sideindex-size) * 2);
}
.top1::before {
content: "1";
}
.top2::before {
content: "2";
}
.top3::before {
content: "3";
}
.top1::before, .top2::before, .top3::before {
margin-inline-start: 7mm;
margin-block: 0mm 2mm;
padding-inline-start: 2.5mm;
inline-size: 14mm;
block-size: 9mm;
border-radius: 5mm 0 0 5mm;
background-color: #6dba44;
color: #fff;
font-size: 20Q;
line-height: 9mm;
writing-mode: horizontal-tb;
}

連番を付ける
節番号や図表番号を自動で連番にさせるためには、連番をリセットするためのcounter-reset
、要素を入れるためのcontent
、連番を増やすためのcounter-increment
などを組み合わせて使用します。挿入する場所は::before
で設定します。
:root {
counter-reset: chapter-counter 0; /* chapter-counterの開始番号を0にリセット */
}
h1 {
counter-reset: section-counter 0; /* section-counterの開始番号を0にリセット */
}
h2::before {
content: counter(chapter-counter) "-" counter(section-counter); /* counter関数(章番号)-counter関数(節番号) */
color: #fff;
counter-increment: section-counter; /* counter関数の変数名(節番号) */
font-size: 48Q;
font-weight: 900;
line-height: 14mm;
text-align: center;
padding-block-start: 27mm;
}

なお、Markdown内に手作業でdivを書き込んでいる場合、その入れ子構造がおかしいためにCSSカウンターがうまく増減しないことあります。CSSの記述に問題ないにもかかわらず、数字が増えない場合はそこを確認してみてください。
また、chapter-counter
やsection-counter
は、Frontmatterで初期値を指定できます。CSSを書き換えなくても、Frontmatter内の初期値を変更するだけで章番号や節番号の振り直しが可能なので、ページ数が多かったり、Markdownが章ごとにあったりする場合などに需要のあるノウハウです。
---
link:
- rel: 'stylesheet'
href: 'css/main.css'
lang: 'ja'
body:
style: "counter-reset: chapter-counter 2" <!-- chapter-counterの初期値指定 -->
---
任意改ページを入れる
-
を3つ書くことで水平線hr
要素)break-after: page;
と組み合わせ、任意改ページの指定として使用しています。
---
hr {
break-after: page;
visibility: hidden;
}
表紙ページを作る
名前付きページ
# コンピュータの<br>重箱の隅
<div class="chapter-lead">
章のリード文。章のリード文。章のリード文。章のリード文。
章のリード文。章のリード文。章のリード文。章のリード文。
</div>
#
@page tobira {
background-color: #88ddff;
@bottom-right {
content: none; /* 柱なし */
}
@bottom-center {
content: none; /* ノンブルなし */
}
}
section:has(h1) { /* h1を含むsection */
page: tobira; /* ページの名前をtobiraに設定 */
}

画像を回り込ませる
画像を本文に回り込ませる場合はfloat
を使用します。例えば、画像を本文の右側に回り込ませたい場合、画像の外側にdiv要素などを追加し、"figure right"
などのクラスを与えておきます。その要素に対してfloat: inline-end
を設定することで、右側に回り込むようになります。
<div class="figure right">

</div>
.figure.right {
float: inline-end;
margin-inline-start: 1em;
}

禁則をコントロールする
禁則処理の設定にはline-break
を使用します。デフォルトでの設定はline-break: auto;
となり、一般的な禁則処理が施されている設定です。セレクタはanywhere
、loose
、normal
、strict
などがあり、記載している順で禁則が強くなります。auto
とnormal
は大体同じような処理をします。コードのような禁則処理を無視して書きたいときにline-break: anywhere;
を使用することで、文字の
auto:既定の改行規則
line-break: auto;

loose:最も制限の少ない改行規則
line-break: loose;

anywhere:改行規則を無視
line-break: anywhere;

字間を調整する
字間を調整するにはletter-spacingを使用します。pなどに適用すれば一括で、テキストをspanに分けて個別に適用すれば1字ずつ調整できます。letter-spacing
のデフォルトは0
なので、0
より値が大きいと字間が空き、0
より値が小さい字間が詰まります。また、font-feature-settings
で字間を調整する方法もあります。これは字形に応じたプロポーショナル詰めを適応する設定となるため、OpenTypeフォントの使用が必要でフォント内の詰め情報がないと効果が発揮されません。OpenTypeフォントを使用したうえで、font-feature-settings
をtext-spacing
と組み合わせると、プロポーショナル詰めかつ和欧文間の空きも調整された綺麗な文字詰めにすることができます。


プログラムのコードリストを載せる
コードリストはバッククォート3つでコードの上下を囲み、上部のクォートから半角空けて言語名を指定することで作成することができます。使用するフォントは等幅のソースコード用フォントが望ましいです。フォントまわりの設定として、white-space: pre-wrap;
や、禁則処理の項目でも説明したline-break: anywhere;
を忘れずに指定しておきましょう。
また、トリプルクォートのあとに
Pythonのコード例
```py
from pathlib import Path
from bs4 import BeautifulSoup
result = '## 目次{#toc role="doc-toc"}\n'
outpath = Path('tocoutput.md')
outpath.write_text(result, encoding='utf-8')
```
<pre class="language-py">
<code class="language-py">
<span class="token keyword">from</span> pathlib <span class="token keyword">import</span> Path
<span class="token keyword">from</span> bs4 <span class="token keyword">import</span> BeautifulSoup
result <span class="token operator">=</span> <span class="token string">'## 目次{#toc role="doc-toc"}\n'</span>
outpath <span class="token operator">=</span> Path<span class="token punctuation">(</span><span class="token string">'tocoutput.md'</span><span class="token punctuation">)</span>
outpath<span class="token punctuation">.</span>write_text<span class="token punctuation">(</span>result<span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span>
</code>
</pre>
pre {
white-space: pre-wrap; /* 空白や改行文字を保持しつつ、テキストを必要に応じて折り返す */
line-break: anywhere; /* 改行規則を無視 */
text-spacing: none; /* 字間調整なし */
font-size: 12Q;
line-height: 18Q;
border: solid 0.5mm #585656;
padding-block: 0.5mm;
padding-inline: 1.5mm;
margin-block: 6mm;
margin-inline: 0;
background-image: url("img/vs_pre_bg.png"); /* ソースコード背景 */
background-size: var(--page-body-width) 9mm;
background-repeat: repeat-y; /* y方向に繰り返し */
background-position-y: 0.5mm;
border-radius: 1mm;
}
/* コード内色分け */
.token.keyword {
color: #f00;
}
.token.string {
color: #00f;
}

数式を入れる
vivliostyleでは、MathJaxというライブラリを使用して、LaTeX形式の数式をレンダリングすることができます。Markdownでの数式の表示方法には、テキストの途中に数式を表示するインライン数式と、block
レベルで表示するディスプレイ数式の2種類があるので、用途に応じて使い分けましょう。インライン数式にしたい場合は、$
で囲み、ディスプレイ数式にしたい場合は$$
で、該当箇所を囲みます。
インライン数式は$y=\frac{1}{x^2}$こんな感じになります。
ディスプレイ数式は$$f(x)=\frac{1}{\sqrt{2\pi}}\exp\left(-\frac{x^2}{2}\right)$$となります。
$$
\begin{eqnarray}
\frac{2}{3} + \frac{1}{5} &=& \frac{10}{15} + \frac{3}{15} \\
&=& \frac{13}{15}
\end{eqnarray}
$$

clip-pathで手軽に図形を作る
簡単な図形であればIllustratorやPowerPointのような他のアプリを使用せず、CSSだけで作成することもできます。行頭にちょっとしたアイコンを入れたい場合や、画像を切り抜きたい場合などで重宝する機能です。clip-path
に使用することができる値は、inset
circle
polygon
path
clip-path
で調べてみてください。
吹き出しを作る
吹き出しは、喋らせるキャラクターのイラスト、吹き出しの囲み、吹き出しのくちばしの3パーツを組み合わせて作っていきます。くちばしの作り方には何通りかありますが、今回は先述したclip-path
を使用して作成していきましょう。
<p class="kaiwa deshi naki">CSS組版ってなんか難しそうで苦手意識があるなぁ……。ハードルも高そう……</p>
<p class="kaiwa sense fum">慣れると意外と簡単だよ! InDesignなどを使用した組版よりも楽な点もあるし、少しずつポイントをおさえて実践していこう!</p>
:root {
--kaiwa-facesize: 21mm; /* イラストのサイズ指定 */
}
.kaiwa + .kaiwa {
margin-block-start: 6mm; /* 吹き出し同士の空き */
}
.kaiwa { /* 吹き出しの囲み */
background-color: #BCE1DF; /* 背景色 */
border-radius: 1mm; /* 半径1mmの角丸 */
margin: 2mm var(--kaiwa-facesize); /* 上下マージン2mm 左右マージンはイラスト分 */
padding: 2mm; /* パディング2mm */
font-size: 12Q; /* 文字サイズ12Q */
line-height: 18Q; /* 行送り18Q */
text-indent: 0; /* インデント0 */
position: relative; /* 絶対参照の基準にする */
inline-size: calc(var(--page-body-width) - var(--kaiwa-facesize) * 2); /* 横幅は版面からイラストサイズを引く */
}
.deshi { /* .deshiに対して */
justify-self: end; /* 右揃え */
}
.sense::after { /* .senseの擬似要素::afterに対して */
content: ""; /* 空のコンテンツ */
background-color: #BCE1DF; /* 背景色 */
clip-path: polygon(50% 0%, 50% 100%, 0% 50%); /* 三角形 */
position: absolute; /* 絶対配置にする */
inset-inline-start: -3mm; /* 左位置-3mm */
inset-block-start: 2mm; /* 上位置2mm */
inline-size: 10mm; /* 幅10mm */
block-size: 5mm; /* 高さ5mm */
}
.sense.fum::before { /* .sense.fumの擬似要素::beforeに対して */
content: ""; /* 空のコンテンツ */
position: absolute; /* 絶対配置にする */
inset-inline-start: calc(var(--kaiwa-facesize) * -1 + 0.6mm); /* 左位置イラストサイズ分引いて0.6mm足す */
inset-block-start: -4mm; /* 上位置-4mm */
inline-size: var(--kaiwa-facesize); /* 幅イラストサイズ */
block-size: 14mm; /* 高さ14mm */
background: url("kaiwaimg/chara2-1.png"); /* イラスト指定 */
background-repeat: no-repeat; /* リピートなし */
background-size: var(--kaiwa-facesize); /* 幅イラストサイズ */
background-position-x: 0%; /* 背景のX座標0 */
background-position-y: -3mm; /* 背景のY座標-3mm */
}
.deshi::after { /* .deshiの擬似要素::afterに対して */
content: ""; /* 空のコンテンツ */
background-color: #BCE1DF; /* 背景色 */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%); /* 三角形 */
position: absolute; /* 絶対配置にする */
inset-inline-end: -3mm; /* 右位置-3mm */
inset-block-start: 2mm; /* 上位置2mm */
inline-size: 10mm; /* 幅10mm */
block-size: 5mm; /* 高さ5mm */
}
.deshi.naki::before { /* .sense.nakiの擬似要素::beforeに対して */
content: ""; /* 空のコンテンツ */
position: absolute; /* 絶対配置にする */
inset-inline-end: calc(var(--kaiwa-facesize) * -1 + 0.6mm); /* 右位置イラストサイズ分引いて0.6mm足す */
inset-block-start: -4mm; /* 上位置-4mm */
inline-size: var(--kaiwa-facesize); /* 幅イラストサイズ */
block-size: 14mm; /* 高さ14mm */
background: url("kaiwaimg/chara1-1.png"); /* イラスト指定 */
background-repeat: no-repeat; /* リピートなし */
background-size: var(--kaiwa-facesize); /* 幅イラストサイズ */
background-position-x: 0%; /* 背景のX座標0 */
background-position-y: -3mm; /* 背景のY座標-3mm */
}

さいごに
今回の記事では、リブロワークスが実作業で使用しているポイントや小技を紹介していきました。もちろん、解説したもの以外にも、CSS組版を円滑に進めるためのポイントは多く存在します。むしろ、今解決できない問題も、アップデートが入るたびに解決されるようになっていくのがCSS組版の良いところです。