用HTML+CSS制作完美的目录

1,836 阅读9分钟

目录的构成

就其核心而言,目录是相当简单的。每一行代表一本书或网页的一个部分,并指出你可以在哪里找到这些内容。通常情况下,这些行包含三个部分。

  1. 该章或该节的标题
  2. 引线(即那些点、破折号或线),在视觉上将标题和页码连接起来
  3. 页码

目录很容易在文字处理工具(如Microsoft Word或Google Docs)中生成,但由于我的内容是用Markdown编写的,然后转化为HTML,这对我来说不是一个好选择。我想要的是能与HTML一起工作的自动化工具,以适合打印的格式生成目录。我还希望每一行都是一个链接,这样就可以在网页和PDF中使用,以便在文件中导航。我还希望在标题和页码之间有点状领导。

于是我开始研究。

我看到了两篇关于用HTML和CSS创建目录的优秀博文。第一篇是Julie Blanc写的"用你的HTML建立目录"。朱莉从事PagedJS的工作,这是一个针对网络浏览器中缺失的分页媒体功能的多元填充,可以正确地将文件格式化为印刷品。我从Julie的例子开始,但发现它对我来说并不完全有效。接下来,我找到了Christoph Grabo的"Responsive TOC leader lines with 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向我指出,当list-style-typenone 时,WebKit浏览器会删除列表语义,所以我需要在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确实可以实现正确的标题。Flexbox确实可以实现正确的标题页对齐。但是,当添加领导时,会有一些棘手的对齐问题,所以我选择了克里斯托夫的方法,使用网格,这也是一个好处,因为它也有助于多行标题。下面是一个单独项目的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 Generated Content for Paged Media规范中定义了一个leader() 功能。然而,就像大部分的分页媒体规范一样,这个函数并没有在任何浏览器中实现,因此它被排除在选项之外(至少在我写这篇文章的时候)。它甚至没有被列在caniuse.com上,可能是因为没有人实现它,也没有计划或信号表明他们会实现。

幸运的是,朱莉和克里斯托夫都已经在他们各自的帖子中解决了这个问题。为了插入点头,他们都使用了一个::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 被隐藏了,所以所有这些额外的点都看不见了。结果是一个漂亮的带点的目录。

然而,还有一些事情需要考虑。

萨拉还向我指出,所有这些点对屏幕阅读器来说都算作文本。所以你会听到什么?"介绍点点点...... "直到所有的点都宣布完毕。这对屏幕阅读器用户来说是一种可怕的体验。

解决方案是插入一个额外的元素,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-numerictabular-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文件。当然,如果你想在小册子或书中使用它,它在印刷中也很好看。