在我的好奇心驱使下,我想为什么不去查看一些热门网站,并了解一下它们是如何实现评论组件的布局。起初,我认为这将是一个简单的任务,但实际并非如此。
我花了很多时间试图理解这是如何工作的,以及如何通过现代 CSS(如 :has、size container queries 和 style queries)来改进它。在本文中,我将引导您了解我的思考过程,并分享我在其中所得到的发现。
简介
以下是我们将要构建的布局。乍一看,它可能看起来很简单,但其中有很多微小的细节。
我们有一个评论,可以嵌套两个更深层次。我在本文中将这些称为“深度”。
图中展示了深度是如何根据每个评论的嵌套级别而变化的。
思考布局
在深入细节之前,我更愿意先着手处理布局,并确保它能很好地运作。这样的做法旨在探索现代CSS解决该问题的潜力。
首先要记住的是HTML标记。评论的结构很适合使用无序列表<ul>。
<ul>
<li>
<Comment />
</li>
<li>
<Comment />
</li>
</ul>
< Comment /> 充当评论组件的占位符。这是两条评论的列表的HTML,没有任何回复。
如果对其中一条评论进行回复,那么将会添加一个新的 <ul>。
<ul>
<li>
<!-- Main comment -->
<Comment />
<ul>
<!-- Comment reply -->
<li>
<Comment />
</li>
</ul>
</li>
<li>
<Comment />
</li>
</ul>
从语义角度来看,上面的描述是合理的。
评论包装器布局 - 填充解决方案
我将标题称为“评论包装器”,以免混淆评论组件本身的含义。在下一节中,我将解释我构建布局以处理评论回复的缩进或间距的想法。
请考虑以下标记:
<ul>
<li>
<!-- Main comment -->
<Comment />
<ul>
<!-- Comment reply -->
<li>
<Comment />
</li>
</ul>
</li>
<li>
<Comment />
</li>
</ul>
我们有两条主评论和一条回复。从视觉角度来看,它将如下所示:
我更倾向于将所有的间距和缩进处理都保留在 <li> 元素上,因为它们充当了评论组件的容器。
使data据属性来处理间距
我首先考虑的是在 <ul> 和 <li> 元素上使用数据属性。
<ul>
<li nested="true">
<!-- Main comment -->
<Comment />
<ul>
<!-- Comment reply -->
<li>
<Comment />
</li>
</ul>
</li>
<li>
<Comment />
</li>
</ul>
在CSS中,我们可以这样做:
li[data-nested="true"],
li[data-nested="true"]
li {
padding-left: 3rem;
}
虽然前面提到的方法是有效的,但是我们可以通过使用CSS变量和样式查询来进一步改进。
使用CSS样式变量查询
我们可以检查容器中是否添加了CSS变量--nested: true,并根据此对子元素进行样式设置。
考虑以下标记,我在 <ul> 元素中添加了内联CSS变量--nested: true。
<ul>
<li style="--nested: true;">
<!-- Main comment -->
<Comment />
<ul style="--nested: true;">
<!-- Comment reply -->
<li>
<Comment />
</li>
</ul>
</li>
<li>
<Comment />
</li>
</ul>
使用样式查询,我可以检查CSS变量是否存在,并根据其来为 <li> 元素添加样式。目前,这个特性只在 Chrome 的实验性版本 Canary 中得到支持。
@container style(--nested: true) {
/* Add spacing to the 2nd level <li> items. */
li {
padding-left: 3rem;
}
}
你提到为什么我更喜欢使用样式查询而不是数据属性的原因:
更易于理解:样式查询采用 @container 语法,简单的文字描述已经足够表达其含义。如果容器中有 --nested: true 的 CSS 变量,那么就应用接下来的样式。
可以组合多个样式查询以更好地控制CSS:通过组合多个样式查询,我们可以更灵活地控制CSS样式。
可以与尺寸容器查询结合使用:如果需要,我们还可以将样式查询与尺寸容器查询结合使用,进一步增强对CSS的控制能力。
评论包装器布局 - 使用CSS Subgrid
另一个解决方案是使用CSS子网格(subgrid)来构建嵌套评论布局。坦率地说,这将需要更多的CSS代码,但是探索新的CSS特性的潜力是非常有趣的。
让我简要地解释一下子网格(subgrid)来给您一个概念。考虑以下CSS网格:
<ul>
<li class="main">
<!-- Main comment -->
<Comment />
<ul>
<!-- Comment reply -->
<li>
<Comment />
<ul>
<li>
<Comment />
</li>
</ul>
</li>
</ul>
</li>
</ul>
li.main {
display: grid;
grid-template-columns: 3rem 3rem 1fr;
}
这将被添加到 <ul> 列表的第一个直接 <li> 元素中。这个网格看起来会像这样:
目前,在CSS网格中,不能将主网格传递给子项目。在我们的情况下,我希望将网格列传递给第一个 <ul>,然后再传递给该 <ul> 的 <li>。
幸好,CSS子网格(subgrid)使得这种操作成为可能。目前,它仅在Firefox和Safari浏览器中可用。Chrome浏览器也在朝这个方向发展!
请参考以下示意图:
首先,我们需要设置主网格如下所示。我们有3列。
@container style(--nested: true) {
li.main {
display: grid;
grid-template-columns: 3rem 3rem 1fr;
.comment {
grid-column: 1 / -1;
}
}
}
.comment 组件将始终跨越整个宽度。这就是为什么我添加了 grid-column: 1 / -1。这意味着:“从第一列到最后一列,让评论组件横跨全部列”。这样做有助于避免在嵌套的每个深度中手动输入列号。类似于这样:
/* Not good */
@container style(--nested: true) {
li.main .comment {
grid-column: 1 / 4;
}
ul[depth="1"] .comment,
ul[depth="2"] .comment {
grid-column: 1 / 3;
}
很好!接下来的步骤是将深度为1的评论放置在主网格内,然后添加子网格并定位内部的 <li> 元素。
@container style(--nested: true) {
ul[depth="1"] {
grid-column: 2 / 4;
display: grid;
grid-template-columns: subgrid;
> li {
grid-column: 1 / 3;
display: grid;
grid-template-columns: subgrid;
}
}
}
最后,我们需要定位深度为2的列表(depth=2)。
@container style(--nested: true) {
ul[depth="2"] {
grid-column: 2 / 3;
}
}
这个解决方案确实有效,但我对其中所有的细节并不太喜欢。一个简单的内边距就可以解决问题。
思考连接线的问题
为了更清楚地显示评论和回复之间的关联,我们可以在主评论和回复之间添加连接线。Facebook团队使用了一个 <div> 元素来处理这些连接线。但是,我们能否尝试一些不同的方法呢?
请参考以下示意图:
你的第一反应可能会误导你:「嗨,这看起来就像一个带有左边框和底部边框以及左下角的边框半径的矩形。」
li:before {
content: "";
width: 30px;
height: 70px;
border-left: 2px solid #ef5da8;
border-bottom: 2px solid #ef5da8;
border-bottom-left-radius: 15px;
}
一开始我在上面的CSS代码中添加了height:..,但我意识到这样做不行。因为我无法准确知道连接线的高度。这是因为在CSS中无法直接根据内容动态调整高度。问题出在这里:我需要确保连接线的底部与第一个回复的头像对齐。
于是我想到可以使用伪元素来实现这个目的。如果那条弯曲的连接线可以分成两部分呢?
我们可以将连接线添加到主评论上,而弯曲的元素则用于表示回复。
接下来,如果我们有另一个回复针对第一个回复呢?以下是一个图示,展示了连接线是如何运作的:
在CSS中,我们需要使用伪元素来实现连接线的效果。在开始编写CSS代码之前,我想强调一下,这条线或弯曲部分将根据整行来定位。
处理添加到主评论的连接线
这是我们要解决的第一个挑战。如果主评论有回复,我们需要为其添加连接线。我们可以使用CSS的 :has 伪类来检查一个 <li> 元素是否包含一个 <ul>,如果是,则应用所需的CSS样式。
请参考以下HTML代码:
<ul style="--depth: 0;">
<li style="--nested: true;">
<Comment />
<ul style="--depth: 1;">
<li>
<Comment />
</li>
</ul>
</li>
<li>
<Comment />
</li>
</ul>
我们需要为每个 <Comment /> 元素应用以下条件的样式:
它是 <li> 元素的直接子元素
<li> 元素有一个 <ul> 作为子元素
父元素的 depth 属性为 0 或 1
下面是如何将上述条件翻译为CSS代码。CSS变量 + 样式查询 + :has 伪类 = 一个强大的条件样式。
@container style(--depth: 0) or style(--depth: 1) {
li:has(ul) > .comment {
position: relative;
&:before {
content: "";
position: absolute;
left: calc(var(--size) / 2);
top: 2rem;
bottom: 0;
width: 2px;
background: #222;
}
}
}
上面的例子展示了为什么我更喜欢使用样式查询而不是HTML数据属性来处理评论和回复的深度。
接下来,我们需要为深度为1的回复添加连接线和弯曲元素。这次,我们将使用 <li> 元素的 :before 和 :after 伪元素。
@container style(--depth: 1) {
li:not(:last-child) {
position: relative;
&:before {
/* Line */
}
}
li {
position: relative;
&:after {
/* Curved element */
}
}
}
最后,我们需要为深度为2的每个 <li> 添加弯曲元素,同时在深度为2的所有 <li> 中除了最后一个之外,都需要添加连接线。我们需要按照以下逻辑进行操作:
为深度为2的每个 <li> 添加弯曲元素。
为深度为2的所有 <li> 中除了最后一个之外的每个 <li> 添加连接线。
弯曲元素是一个带有边框和左下角半径的矩形。让我来解释一下:
@container style(--depth: 2) {
li {
position: relative;
&:after {
content: "";
position: absolute;
inset-inline-start: 15px;
top: -2px;
height: 20px;
width: 28px;
border-inline-start: 2px solid #000;
border-bottom: 2px solid #000;
border-end-start-radius: 10px;
}
}
li:not(:last-child) {
&:before {
/* Line */
}
}
}
请注意,我在边框(border)和边框圆角(border-radius)方面使用了逻辑属性。这样做有助于在文档语言为RTL(从右到左)时动态翻转用户界面。我将在文章后面详细介绍这个内容。
禁用连接线
如果出于某种原因我们需要隐藏连接线,那么通过样式查询(style queries)来实现这一点就像切换CSS变量的开关一样简单。
通过将所有与深度相关的样式查询嵌套在 --lines: true 的样式查询内部,我们可以确保只有在设置了该 CSS 变量时才会显示连接线。
@container style(--lines: true) {
@container style(--depth: 0) {
}
@container style(--depth: 1) {
}
@container style(--depth: 1) {
}
@container style(--depth: 2) {
}
}
评论组件
你可能会认为上面所有的内容都只是用于主要的布局和连接线。是的,没错!我甚至还没有考虑评论组件。
让我们仔细看一下评论组件:
乍一看,这似乎是使用 flexbox 的绝佳场景。我们可以通过 flexbox 将头像和评论框显示在同一行上。
<div class="comment">
<div class="user"></div>
<!-- Because an additional wrapper doesn't hurt. -->
<div>
<div class="comment__body"></div>
<div class="comment__actions">
<a href="#">Like</a>
<a href="#">Reply</a>
</div>
</div>
</div>
请注意,上面的HTML代码非常基础,它并不代表生产级别的代码,只是用来帮助解释CSS的内容。
.comment {
--size: 2rem;
display: flex;
gap: 0.5rem;
}
.avatar {
flex: 0 0 var(--size);
width: var(--size);
height: var(--size);
border-radius: 50%;
}
这是基本的布局,但实际应用中可能会更加复杂。然而,在本文中,我将仅专注于需要解释的独特和重要的内容。
接下来,我们会讨论评论主体组件的一些考虑事项。
评论组件的这部分将需要处理以下内容:
最小宽度
长内容
多语言内容(左到右 vs 右到左)
上下文菜单
评论交互
编辑状态
错误状态
我在这篇文章中无法详细展示上述所有内容,因为可能需要写一本书来完整讲述。
我将重点介绍一些我认为适合使用现代CSS的有趣技巧。
改变用户头像大小
在回复嵌套在评论中时,用户头像的大小将变小。这样做有助于在视觉上更容易区分主评论和回复。
使用样式查询是非常适合这种情况的。
.user {
flex: 0 0 var(--size);
width: var(--size);
height: var(--size);
}
.comment {
--size: 2rem;
@container style(--depth: 1) or style(--depth: 2) {
--size: 1.5rem;
}
}
动态文本对齐与 dir=auto 属性
评论可能包含从左到右(LTR)或从右到左(RTL)的语言。根据内容的语言,文本对齐应该有所区别。感谢 dir=auto HTML 属性,我们可以让浏览器自动处理这一点。
<div class="comment">
<div class="user"></div>
<div>
<div class="comment__body">
<p dir="auto"></p>
</div>
<div class="comment__actions"></div>
</div>
</div>
CSS 逻辑属性
通过使用 CSS 逻辑属性,我们可以构建评论组件,使其能根据文档的方向进行自适应调整。同样的原理也适用于连接线。
表情符号回复状态
当用户添加仅由表情符号组成的评论时,评论容器将会有一些变化:
没有背景颜色
没有内边距
这是使用CSS :has伪类的一个绝佳用例。
.comment:has(.emjois-wrapper) {
background: var(--default);
padding: var(--reset);
}
结论
现代CSS的潜力一直让人兴奋不已。尝试用新的方式思考已经构建的组件或布局,是学习新知识的绝佳途径。我在整个过程中学到了很多新东西,并享受了整个过程。
由于文章内容篇幅有限,今天的内容就分享到这里,文章结尾,我想提醒您,文章的创作不易,如果您喜欢我的分享,请别忘了点赞和转发,让更多有需要的人看到。同时,如果您想获取更多前端技术的知识,欢迎关注我,您的支持将是我分享最大的动力。我会持续输出更多内容,敬请期待。