编辑注意:这篇文章只是一个使用新 CSS 属性的实验,所以下面的代码不应在没有对可访问性进行重大改进的情况下使用。
我有一种奇怪的 图表强迫症,出于某种原因,我想找出用 CSS 制作图表的所有方法。 我想有两个原因。 首先,我认为用 CSS 在网络上设置图表和数据的方式有数百万种,这很有趣。 其次,这对学习新技术和不熟悉的技术非常有用。 在这种情况下:CSS Grid!
所以,我对图表这种痴迷让我思考:如何使用 CSS Grid 制作一个简单的响应式条形图,就像这样
让我们看看我是如何实现的!
快速简便的方法
由于 Grid 乍一看可能令人困惑和奇怪,因此让我们专注于首先创建一个非常简陋的原型。 为了开始,我们需要编写我们图表的标记
<div class="chart">
<div class="bar-1"></div>
<div class="bar-2"></div>
<div class="bar-3"></div>
<div class="bar-4"></div>
<!-- all the way up to bar-12 -->
</div>
每个 bar-
类将构成我们图表中的一整条,而且,尽管这可能看起来很糟糕,但现在我们不会太担心语义或标记网格或数据。 这些稍后再说 - 让我们专注于 CSS,以便我们可以更多地了解 Grid。
好的,有了这些,我们现在可以进行样式设置了! 我们图表中需要 12 根条形图,它们之间有 5px 的间隙,所以我们可以使用相关的 CSS Grid 属性设置父类 .chart
.chart {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-column-gap: 5px;
}
如果您熟悉 Grid,这非常简单,但它实际上描述了以下内容:“我想要 12 列,每个子元素具有相等的宽度(1 分数),它们之间有 5px 的间隙”。
但是,现在,这里有一个巧妙的部分:使用 Grid,我们可以使用 grid-template-rows
属性来设置我们图表中每根条形图的高度
.chart {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(100, 1fr);
grid-column-gap: 5px;
}
我们可以使用那个整洁的新属性在我们网格中创建 100 行,这样我们就可以将每根条形图设置为该高度的百分比,这将使计算变得简单。 同样,我们使用 repeat()
函数,以便我们每一行都具有相同的高度。
在我详细解释之前,让我们为图表指定一个最大宽度,并使用 flex 将其设置为屏幕中央
* { box-sizing: border-box; }
html, body {
margin: 0;
background-color: #eee;
display: flex;
justify-content: center;
}
.chart {
height: 100vh;
width: 70vw;
/* other chart styles go here */
}
此时,我们的图表仍然是空的,因为我们还没有告诉我们的子元素在网格中占用任何空间。 所以让我们修复它! 我们将选择包含 bar
的每个类,并使用 grid-row-start
和 grid-row-end
属性使它们填充网格中的垂直空间,因此最终我们将更改其中一个属性来定义每个条形图的自定义高度
[class*="bar"] {
grid-row-start: 1;
grid-row-end: 101;
border-radius: 5px 5px 0 0;
background-color: #ff4136;
}
所以,如果您对这些 grid-row
属性感到困惑,那就没问题! 我们告诉每个条形图从网格的最顶端(1)开始,然后在最底部(101)结束。 但是,当我们只告诉网格包含 100 行时,为什么我们要将 101 用作该属性的值? 在我们继续之前,让我们稍微探索一下!
网格线
在进行这个演示之前,我没有考虑过关于 Grid 的一个奇怪的事情,那就是 网格线 的概念,这对理解这个新的布局工具非常重要。 以下是四列四行网格中如何绘制网格线的图示

这个 新示例 包含四列和四行,具有以下样式
.grid {
grid-gap: 5px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
}
.special-col {
grid-row: 2 / 4;
background-color: #222;
}
grid-row
是 grid-row-start
和 grid-row-end
的简写属性,第一个值是我们希望元素在网格中开始的位置,第二个值是我们希望它结束的位置。 但是! 这意味着我们希望此处的特殊元素从网格线 2 开始,并在网格线 4 结束 - 而不是在第 4 行的末尾。 如果我们希望那个黑色方块填充所有 4 行,那么它需要在第 5 行结束,或者 grid-row: 2 / 5
,如果您仔细想想,这很有道理。
换句话说,我们不应该认为元素占用了网格的整行或整列,而应该认为它们只跨越了这些网格线。 在深入研究 Jen Simmons 最近关于此问题的教程 之后,我花了一段时间才在概念上理解并习惯这一点。
无论如何!
回到演示
所以这就是为什么在我们的图表演示中,我们将所有列都放在第 101 行而不是第 100 行的原因 - 因为我们希望它填充最后一行(100),所以我们必须将其发送到该特定网格线(101)。
现在,由于我们的 .chart
类使用 vw/vh 单位,我们也有一个很好的响应式图表,而无需进行太多操作。 如果您调整下面的图表大小,您会发现它会很好地收缩或拉伸,始终占用整个视窗
从这里,我们可以开始为每个单独的条形图设置样式,以赋予它们正确的数据,而且我们可以用很多不同的方法来做到这一点。 让我们看其中一种方法。
首先,让我们假设我们希望图表中的第一个条形图 .bar-1
占据图表高度的 50/100 或者一半。 我们可以编写以下 CSS 并完成它
[class*="bar"] {
grid-row-end: 101;
}
.bar-1 {
grid-row-start: 50;
}
这看起来不错! 但是,这里有一个问题 - 我们使用 grid-row-start
实际上声明的是让条形图从“50”开始,并在“101”结束,但这并不是我们真正想要的。 举个例子:假设在这个假设的例子中数据发生了变化,我们需要将它改为 20/100。 让我们回去更改该值
.bar-1 {
grid-row-start: 20;
}
这是错误的! 我们希望条形图不要从网格中的 30 开始,而是占据图表高度的 30%。 我们可以将我们的值更改为 grid-row-start: 20;
,或者我们可以改为使用 grid-row-end
属性,对吗? 好吧,不完全是
.bar-1 {
grid-row-end: 20;
}
条形图的大小是正确的,但位置是错误的,因为我们告诉条形图在 30/100 处结束。 那么我们如何解决这个问题并使我们的代码超级易读呢? 好吧,一种方法是利用 Sass 为我们进行计算。 理想情况下,我们希望编写类似以下内容
.bar-1 {
// makes a bar that's 60/100 and positioned at the bottom of our chart
@include chartValue(60);
}
无论我们在该 mixin 中输入什么值,我们都希望始终获得图表在网格上的正确高度和位置。 为这个 mixin 提供动力的计算实际上非常简单:我们只需要取我们的值,从总行数中减去它,然后将其附加到 grid-row-start
属性,像这样
@mixin chartValue($data) {
$totalRows: 101;
$result: $totalRows - $data;
grid-row-start: $result;
}
.bar-1 {
@include chartValue(20);
}
所以,我们的 Sass mixin 生成的最终值为 grid-row-start: 81
,但我们的代码非常清晰! 我们甚至不必查看网格就知道会发生什么 - 条形图将放置在网格的底部,并且该值将始终是正确的。
那么,我们如何创建所有这些网格类呢? 我认为一种不错的方法是让 Sass 为我们自动生成所有这些类。 只需对我们的代码进行一些修改,我们就可以执行类似的操作
$totalRows: 101;
@mixin chartValue($data) {
$result: $totalRows - $data;
grid-row-start: $result;
}
@for $i from 1 through $totalRows {
.bar-#{$i} {
@include chartValue($i);
}
}
这将遍历我们图表中的所有行,并为该行大小生成一个单独的类。 因此,现在我们可以像这样更新我们的标记
<div class="bar-45"></div>
<div class="bar-100"></div>
<div class="bar-63"></div>
<div class="bar-11"></div>
就是这样! 我们不必手动为每个元素编写单独的类,而且我们可以通过简单地更改标记轻松地更新图表。 这个 Sass 循环将输出很多未使用的类,但有很多工具可以删除这些类。
我们可以在网格中做的最后一件事是通过奇偶数为每一列设置颜色
[class*="bar"]:nth-child(odd) {
background-color: #ff4136;
}
[class*="bar"]:nth-child(even) {
background-color: #0074d9;
}
就是这样! 一个使用 CSS Grid 制作的响应式图表。 但是,我们可以做很多事情来整理这段代码。 我们可能应该做的第一件事是确保我们使用语义标记,并使用工具删除由我们的 Sass 循环输出的所有这些类。 我们还可以深入了解此图表在移动设备上的呈现方式,并考虑我们应该如何标记每一列和图表轴。
但就目前而言,这只是一个开始。 这篇文章的 TL;DR:CSS Grid 可用于各种事情,而不仅仅是将文本和图像并排放置。 它为我们打开了 Web 设计的一个全新分支,我们可以进行实验。
如何使用自定义数据属性和 attr() 来设置高度?
不支持。
我之前也想过这个主意……不幸的是,它似乎并没有得到广泛支持。 attr() 的使用似乎在伪元素的 content 属性中最有效,仅此而已。 :/
真棒!
就我个人而言,我不太喜欢将数据放在我的类名中,但这很容易解决……
而不是
只需执行以下操作即可
并相应地调整选择器。
嗨,Robin,你认为这种方法仅仅是用 CSS Grid 做的有趣的实验,还是你认为它比使用 flexbox 制作基于 CSS 的条形图更具优势?
作为参考,以下是我使用 flexbox 的方法
我喜欢你的方法。感谢分享。
类似的结果,不使用网格… 这是我想出来的,只是为了好玩
不推荐实际使用… 其中一件事是条形之间的间距并不总是保持一致
我之前用 div 做过类似的事情。可以简化和改进,但对于这种事情来说,网格似乎有点过头了。
https://codepen.io/Jpburns/full/mApbYk/
正如 Hugo Giraudel 在 Twitter 上 指出的那样,这是一个很酷的视觉效果,但就目前而言,这个“图表”完全不可访问。数据仅通过 CSS 样式公开,没有其他方式。
一些人回复说 SVG 会更好。我不同意。SVG 本身并没有自动语义化。
<rect>
的含义不比<div>
多。要使此图表中的数据具有语义意义,您需要
清楚地表明您有一组不同的数据值
将数值作为该集合中每个项目的可访问名称的一部分进行传达。
有一些方法可以将该信息添加到 SVG 中。但您也可以将其添加到 HTML 元素中。
对于第一部分(说明这是一个项目列表),您可以简单地使用适合项目列表的语义 HTML 元素(
<ol>
和<li>
)来实现。对于第二部分,您要么需要将数据作为 HTML 元素的文本内容包含在内,要么使用 ARIA 添加标签。这是我的方法
我从 Robin 的最终代码中进行了更改,以使用语义 HTML(一个项目列表),将数据值与类分开,并为值添加了可见和屏幕阅读器可访问的标签。这也意味着即使不支持网格,数据也是可见的。
当然,您仍然不能将此图表复制粘贴到网站中,因为它仍然没有解释数据是什么的标签或上下文。但这是一个单独的问题。关键变化是,每个人现在将获得相同数量的(无上下文)信息。
可访问性仍然不是理想的。我不太喜欢对原始数字使用
aria-label
。我最初使用了data-*
属性。但许多屏幕阅读器不会读取 CSS 生成的内容标签。使用aria-label
属性会使它们更有可能将值作为每个列表项目的名称来读取。但对于一个真正的条形图来说,您可能需要标签包括x轴值。这使得在 CSS 选择器和生成的內容中使用
aria-label
属性变得困难。基本上,在任何实际情况中,您都需要复制值:一次用于可访问的标签,以人类可读的格式进行格式化,一次用于您用来定义样式和大小的属性或自定义属性。
我认为这确实是一种真正可访问的方法。
从那里开始,我将添加我的想法:为什么不将实际内容(值)作为内容?但标签有什么用呢?当然是指值的含义!
CSS 基本上是一样的,只是在 HTML 中添加了内容以及 ::before 和 ::after 类,以便显示数据。
Amelia,这是一个很好的说明!我们还有即将发布的帖子,它将更深入地介绍您所关注的可访问性问题,并进一步发展我的想法。
太棒了!
您还可以对
grid-row-end
使用负值;基本上是从末尾开始说“1”。很棒的帖子!
不过,有一件事让我很困惑,为什么在这些行定义
grid-template-rows: repeat(100, 1fr);
中,grid-row-end: 101;
和grid-row-end: 102;
之间存在差异?我刚回来这里要问同一个问题!不明白为什么当我使用 grid-row-end: 101 时,条形底部会有 1px 左右的间隙(在我给图表添加边框时注意到了)。用 102 替换 101 来修复它,但仍然让我感到困惑的是,为什么 101 不起作用。
嗨 Robin。感谢您的详细解释。因为我刚开始学习 CSS 网格,我以为我无法理解它,但你写得非常好,即使像我这样的新手也能够理解它。
如果可以简单地反转行的 grid-auto-flow 顺序,以便它们从底部开始…就像可以反转 flex-direction 一样,那就太酷了。
实际上,仔细想想… 一种实现此目标的脏方法是简单地使用 transform: scaleY(-1)… 但如果需要在同一个容器中添加其他元素(例如文本),这可能会造成问题。
我认为,通过使用
<meter>
(横向旋转)可以同时获得可访问性优势和根据数据自动调整大小。它甚至可以接收<label>
!使用 CSS 网格,您可以使用负值来从底部到顶部定位盒子。这是一种比使用 SASS 计算值更好的方法。
连续值怎么办?0.9%,0.99%,0.999%,1.3434%,2.54354% 等等?创建像
.bar-0.9854"
这样的 CSS 选择器?