使用 HTML + CSS 制作完美的目录

Avatar of Nicholas C. Zakas
Nicholas C. Zakas

DigitalOcean 提供适合您旅程各个阶段的云产品。立即开始使用 价值 200 美元的免费信用额度!

今年早些时候,我自出版了一本名为《理解 JavaScript Promises》的电子书 (免费下载)。尽管我无意将其制作成纸质书,但还是有很多人询问纸质版本,于是我决定也自出版纸质版本。我认为使用 HTML 和 CSS 生成 PDF 并发送给印刷商将是一项简单的任务。我当时没有意识到,我没有解决纸质书的一个重要部分:目录。

目录的构成

从本质上讲,目录非常简单。每一行代表书籍或网页的一部分,并指示您可以在哪里找到该内容。通常,这些行包含三个部分

  1. 章节或部分的标题
  2. 引导符(即那些点、破折号或线),它们在视觉上将标题连接到页码
  3. 页码

目录很容易在 Microsoft Word 或 Google Docs 等文字处理工具中生成,但由于我的内容是 Markdown 格式并转换成了 HTML,因此对我来说这不是一个好的选择。我想要一种自动化的方式,可以使用 HTML 生成适合打印的目录格式。我还希望每一行都是链接,以便它可以在网页和 PDF 中用于在文档中导航。我还希望标题和页码之间有引导符。

因此我开始研究。

我偶然发现了关于使用 HTML 和 CSS 创建目录的两个优秀博文。第一篇是 Julie Blanc 的 “从您的 HTML 中构建目录”。Julie 曾在 PagedJS 工作,PagedJS 是一个针对网页浏览器中缺少的分页媒体功能的 polyfill,它可以适当地格式化打印文档。我从 Julie 的示例开始,但发现它并不适合我。接下来,我找到了 Christoph Grabo 的 “使用 CSS 创建响应式目录引导符” 文章,这篇文章介绍了使用 CSS Grid(而不是 Julie 的基于浮动的方案)来简化对齐方式的概念。但同样,他的方案也不完全适合我的目的。

不过,阅读了这两篇文章后,我感觉自己对布局问题有了足够的了解,可以开始自己尝试了。我借鉴了这两篇博文的片段,并添加了一些新的 HTML 和 CSS 概念到方案中,最终得出了一个令我满意的结果。

选择正确的标记

在决定目录的正确标记时,我主要考虑了正确的语义。从根本上讲,目录是关于标题(章节或小节)与页码的绑定,几乎就像一个键值对。这让我想到了两种选择

  • 一种选择是使用表格 (<table>),一列用于标题,一列用于页码。
  • 然后是经常被遗忘的定义列表 (<dl>) 元素。它也充当键值映射。因此,标题和页码之间的关系将再次变得显而易见。

这两种方案似乎都是不错的选择,直到我意识到它们实际上只适用于单级目录,也就是说,如果我只想要一个包含章节名称的目录,那么它们就能发挥作用。但是,如果我想在目录中显示小节,那么我就没有好的选择。表格元素不适合分层数据,虽然定义列表在技术上可以嵌套,但语义似乎并不正确。因此,我重新开始思考。

我决定借鉴 Julie 的方案并使用列表;但是,我选择了有序列表 (<ol>) 而不是无序列表 (<ul>)。我认为有序列表在这种情况下更合适。目录代表章节和小标题的列表,按照它们在内容中出现的顺序排列。顺序很重要,不应该在标记中丢失。

不幸的是,使用有序列表意味着丢失了标题和页码之间的语义关系,因此我的下一步是重新建立列表项中的这种关系。解决此问题的最简单方法是在页码之前简单地插入“页”字。这样,即使没有其他视觉区分,数字相对于文本的关系也是明确的。

以下是一个简单的 HTML 骨架,它构成了我的标记的基础

<ol class="toc-list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page">Page 1</span>
    </a>

    <ol>
      <!-- subsection items -->
    </ol>
  </li>
</ol>

将样式应用于目录

确定了要使用的标记后,下一步是应用一些样式。

首先,我删除了自动生成的数字。如果您愿意,可以选择在自己的项目中保留自动生成的数字,但书籍通常会在章节列表中包含无编号的前言和后记,这使得自动生成的数字不正确。

出于我的目的,我会手动填写章节编号,然后调整布局,使顶级列表没有任何填充(因此与段落对齐),并且每个嵌套列表缩进两个空格。我选择使用 2ch 填充值,因为我还不确定要使用哪种字体。ch 长度单位允许填充相对于字符宽度进行调整——无论使用哪种字体——而不是可能导致外观不一致的绝对像素大小。

以下是我最终使用的 CSS 代码

.toc-list, .toc-list ol {
  list-style-type: none;
}

.toc-list {
  padding: 0;
}

.toc-list ol {
  padding-inline-start: 2ch;
}

Sara Soueidan 指出,WebKit 浏览器在 list-style-typenone 时会删除列表语义,因此我需要在 HTML 中添加 role="list" 来保留它

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page">Page 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

设置标题和页码的样式

对列表进行样式设置后,是时候继续设置单个列表项的样式了。对于目录中的每个项目,标题和页码都必须在同一行上,标题位于左侧,页码与右侧对齐。

您可能会想,“没问题,这就是 flexbox 的作用!”您说得没错!Flexbox 确实可以实现正确的标题-页码对齐方式。但是,在添加引导符时,存在一些棘手的对齐问题,因此我选择使用 Christoph 的方案,使用网格,它也有助于处理多行标题。以下是对单个项目设置样式的 CSS 代码

.toc-list li > a {
  text-decoration: none;
  display: grid;
  grid-template-columns: auto max-content;
  align-items: end;
}

.toc-list li > a > .page {
  text-align: right;
}

网格有两列,第一列是 auto 大小,用于填充容器的整个宽度,减去第二列,第二列的大小为 max-content。页码与右侧对齐,这在目录中是传统的。

此时我做的唯一其他更改是隐藏“页”字。这对屏幕阅读器很有帮助,但在视觉上没有必要,因此我使用了一个 传统的 visually-hidden 来将其隐藏起来

.visually-hidden {
  clip: rect(0 0 0 0);
  clip-path: inset(100%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  width: 1px;
  white-space: nowrap;
}

当然,HTML 也需要更新以使用该类

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title</span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

有了这个基础,我继续处理标题和页码之间的引导符。

创建引导符

引导符在印刷媒体中如此常见,您可能会想,为什么 CSS 还不支持它?答案是:**支持,但不是完全支持。**

CSS 生成内容用于分页媒体规范中实际上定义了一个 leader() 函数。但是,与大多数分页媒体规范一样,此函数在任何浏览器中都没有实现,因此将其排除在选项之外(至少在我撰写本文时是如此)。它甚至没有在 caniuse.com 上列出,可能是因为还没有人实现它,并且没有计划或信号表明他们会实现它。

幸运的是,Julie 和 Christoph 已经在各自的文章中解决了这个问题。为了插入引导符,他们都使用了一个 ::after 伪元素,并将它的 content 属性设置为一个非常长的点字符串,例如

.toc-list li > a > .title {
  position: relative;
  overflow: hidden;
}

.toc-list li > a .title::after {
  position: absolute;
  padding-left: .25ch;
  content: " . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . ";
  text-align: right;
}

::after伪元素被设置为绝对定位,以将其从页面流中取出并避免换行。文本对齐到右边,因为我们希望每行最后的点与行尾的数字对齐。(稍后将详细介绍此复杂性的更多内容。).title元素被设置为相对定位,以便::after伪元素不会超出其框。同时,overflow被隐藏,所以所有额外的点都不可见。结果是一个带有点引导的漂亮目录。

但是,还需要考虑其他事项。

Sara还向我指出,所有这些点对屏幕阅读器来说都算作文本。那么你听到什么呢?“引言点点点……”直到所有点都被宣布。这对屏幕阅读器用户来说是一种糟糕的体验。

解决方案是插入一个额外的元素,将aria-hidden设置为true,然后使用该元素插入点。所以 HTML 代码变为

<ol class="toc-list" role="list">
  <li>
    <a href="#link_to_heading">
      <span class="title">Chapter or subsection title<span class="leaders" aria-hidden="true"></span></span>
      <span class="page"><span class="visually-hidden">Page</span> 1</span>
    </a>

    <ol role="list">
      <!-- subsection items -->
    </ol>
  </li>
</ol>

而 CSS 代码变为

.toc-list li > a > .title {
  position: relative;
  overflow: hidden;
}

.toc-list li > a .leaders::after {
  position: absolute;
  padding-left: .25ch;
  content: " . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . "
      ". . . . . . . . . . . . . . . . . . . . . . . ";
  text-align: right;
}

现在屏幕阅读器会忽略这些点,从而避免用户因听到多个点被宣布而感到沮丧。

收尾工作

此时,目录组件看起来已经相当不错,但还需要一些细微的调整。首先,大多数书籍在视觉上将章节标题与小节标题区分开来,因此我将顶级项目加粗,并引入了一个边距来将小节与后面的章节隔开。

.toc-list > li > a {
  font-weight: bold;
  margin-block-start: 1em;
}

接下来,我想清理一下页码的对齐方式。当我使用固定宽度的字体时,一切看起来都很好,但对于可变宽度的字体,引导点可能会形成锯齿状图案,因为它们会根据页码的宽度进行调整。例如,任何页码包含数字 1 的页码都将比其他页码窄,导致引导点与前一行或后一行的点不对齐。

Misaligned numbers and dots in a table of contents.

为了解决这个问题,我将font-variant-numeric设置为tabular-nums,这样所有数字的宽度都相同。通过还将最小宽度设置为2ch,我确保了所有包含一位或两位数字的数字都完美对齐。(如果您的项目有超过 100 页,您可能需要将其设置为3ch。)以下是页码的最终 CSS 代码。

.toc-list li > a > .page {
  min-width: 2ch;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
Aligned leader dots in a table of contents.

就这样,目录就完成了!

结论

用纯 HTML 和 CSS 创建目录比我预期的更具挑战性,但我对结果非常满意。这种方法不仅足够灵活,可以容纳章节和小节,而且可以很好地处理子小节,而无需更新 CSS。总体方法适用于您想要链接到内容不同位置的网页,以及您想要目录链接到不同页面的 PDF 文件。当然,如果您想在手册或书籍中使用它,它在打印时也看起来很棒。

我要感谢 Julie Blanc 和 Christoph Grabo 他们撰写的关于创建目录的优秀博文,这两篇博文在我开始时都非常宝贵。我还要感谢 Sara Soueidan 在我进行此项目时提供的无障碍反馈。