几乎可访问* 动画手风琴:纯 CSS 实现???没门!😱

2025-06-08

几乎可访问* 动画手风琴:纯 CSS 实现???没门!😱

几天前,我用 1 分钟、5 分钟和 10 分钟的时间设计了一个手风琴面板,它比大多数面板更容易使用!(完美?不完全完美,但辅助技术用户和仅使用键盘的用户肯定可以使用)。

但让我恼火的是,我必须使用 JavaScript 才能使动画正常工作。

我们都知道人们有多喜欢纯 CSS 的东西(不要让我开始谈论那些使用 React 或 Vue 构建网站然后只想要 CSS 的东西的人......)

更新

请注意,我在这里犯了几个大错误,作为一个自称无障碍专家的人,这真是令人尴尬。所以现在标题上写着“几乎无障碍”……我本来很接近了,但失败了!

然而,当我错了的时候,我很乐意举手。

需要考虑的几件事:

  1. 由于使用了 ,这在 Firefox 中不起作用:has。我正在考虑重构代码,使其不再依赖于:has,但就目前而言,这意味着它不适合在没有 polyfill 的情况下用于生产(这有点违背了目的,因为这本来就意味着没有 JS!)。
  2. 由于使用了链接,使用屏幕阅读器时的行为可能与预期不符。我个人意识到了这一点,但显然我没有给予足够的重视或思考。我将更新 CodePen,尽可能地解决这个问题。
  3. 由于使用了 Fragment,此模式每次打开某个部分时都会更新浏览器历史记录。这体验不佳!感谢@starkraving指出这一点。

因此,我需要将我最初的建议(最后的手段模式)升级为“不要在生产中使用这种模式!”。

相反,将其用作 CSS 技术的学习资源,您可能会发现在某些情况下很有用。

我很抱歉错过了一些我应该注意到的明显的事情,我会加倍努力,确保在我应该更了解的时候不会将某些东西标记为可访问。

特别感谢@merri纠正我并花时间指出我的错误,非常感谢!

随着我重新思考和改进,我会进一步更新这篇文章,我或许能够将其重新定义为“可访问的”……但如果我这样做了,我一定会确保这次我做对了!💗

手风琴演示

用键盘试试吧!使用 Tab 键在各个部分之间切换,使用上下箭头键滚动可滚动部分(在移动设备上只需拖动即可!),你甚至可以使用复选框打开所有部分!

最重要的是,仔细研究一下 CSS 和 HTML,看看你发现了什么!

乍一看可能很简单,但这里有很多事情要做!

以下是最有趣的部分:

  • 使用该:target属性确保一次只打开一个项目。
  • 使用 CSS grid“hack”让我们拥有任意高度的动画部分(这曾经是困难的事情!)
  • 另一个技巧(这次是真正的技巧)允许我们仅在内容需要时显示滚动条,而不会在部分扩展时显示。
  • 使用prefers-reduced-motion简单的模式来删除动画CSS var
  • 此模式支持一个简单的@media print媒体查询,而其他手风琴模式不支持打开所有面板进行打印(并允许它们不受高度限制地扩展,因此所有内容都可以打印)!
  • 如果需要,可以通过复选框打开和关闭所有手风琴部分。

那么,让我们看看每一个部分!

属性:target

这个属性很有趣,事实上,它是这个手风琴能够运作的全部原因!

它允许我们使用 CSS 选择某些内容,只要它具有与 URL 末尾的哈希相同的 ID。

好的,这可能不是立即有意义的,所以让我解释一下。

假设我们有一个带有 的 div id="blue"

我们可以创建一个 CSS 选择器,如下所示:

div{
  background-color: red;
}


div:target{
  background-color: blue;
}
Enter fullscreen mode Exit fullscreen mode

因此,当我们将页面加载到其默认 URL(例如 example-site.com)时,该 div 将变为红色。

但是,如果我们将 URL 更新为 example-site.com* #blue *,那么该 div 将变成蓝色!

还有一种更新 URL 的简单方法,我们可以使用锚<a>标记并将其设置href为等于该 ID(<a href="#blue">)。

我把所有这些代码都放在一个简单的 CodePen 里。注意,这段 JS 代码只是为了显示 CodePen 的 URL,因为你看不到它。点击链接时,请注意 URL 的结尾。

我们可以利用这一点,让我们的手风琴独一无二!

通过为每个手风琴“部分”分配一个 ID,然后使用<a>带有该 ID 的锚 () 标签href,我们现在可以在 CSS 中仅选择一个部分而不选择其他部分。

然后,如果我们点击属于另一个部分的链接,选择器将改为定位该部分。

演示中的相关代码(简化)如下:

HTML

<a href="#acc1">Section 1 "Heading"</a>
<section class="content" id="acc1">
  <p> The content </p>
</section>

<!-- the `href` matches the `id` of the section below it. -->  
<a href="#acc2">Section 2 "Heading"</a>
<section class="content" id="acc2">
  <p> The content </p>
</section>
Enter fullscreen mode Exit fullscreen mode

CSS

.content{
  display: none;
}

.content:target{
  display: block   
}
Enter fullscreen mode Exit fullscreen mode

把所有这些放在一起,您现在应该能够理解:target属性行为了!

这还有一个好处!

使用片段标识符(URL 末尾的哈希名称)还有另一个好处,我们用锚元素进行更新……它允许我们将焦点移动到页面上!

当我们添加#acc1到 URL 时,页面会跳转到具有相同 ID 的部分。这在本例中非常有用,因为我们可能有一个可滚动的区域,我们希望用户能够使用箭头键上下滚动。

将焦点移到那里意味着当手风琴打开时,他们可以立即使用向上和向下箭头键,而不必Tab转到可滚动部分。

一次出色的快速用户体验胜利!

用于高度动画的 CSS 网格

如果您曾经尝试为高度不固定的物品制作动画……那么您将会感到沮丧和痛苦。

“我不能只为高度制作动画吗”...不,那样不行吗?

“动画怎么样max-height?” - 有点用,但动画时间会根据每个项目的高度而变化,最终会出现奇怪的延迟(因为整体max-height都是动画,即使你只使用其中的一小部分)。

不,这个问题曾经只能通过固定高度或 JavaScript 才能正确解决。

但是,grid现在已经有了...并且网格最终可以给我们所需要的东西!

谢谢网格行和visibility: hidden

那么动画开场的神奇秘诀是什么呢?

首先:grid-template-rows: min-content 0fr

等一下...这是什么意思?

  • grid-template-rows允许我们定义网格中行的行为方式。每个空格分隔的条目按顺序对应一行。
  • min-content表示此行应尽可能小以显示内容。如果我们将其用于段落的列(而不是行),那么最长的单词最终将等于容器的宽度,并且单词将相应地换行。可以将其视为在给定方向上“尽可能小”!
  • 0fr是最后一部分,我们说对于 DOM 顺序中的第二项,使其为“零分数”(fr=“fraction”)0fr。这将使其尽可能小,并且仅在视觉显示时可见。

最后一部分解释了为什么我们需要visibility: hidden对任何想要展开/折叠的项目进行操作。虽然这在视觉上隐藏了该项目,但它仍然会占用 DOM 中的空间。

因此,作为最后一步,我们必须overflow: hidden对其进行处理以确保其折叠,margin: 0padding: 0确保边距和填充不占用空间并且一切正常!

现在,在我向您展示它的工作原理之前,我们需要做最后一件事,我们需要为元素的扩展添加动画!

最后两步!

因此,我们需要做的最后一件事是反转隐藏并让显示的项目占据其全部高度,然后我们就可以为其制作动画了!

所以显然,visibility: hidden需要变成visibility: visible这样,这样我们才能展示出揭示的项目。

然后我们需要改变父级,以便我们有:

grid-template-rows: min-content 1fr

变化量为1fr(1 分) 而不是0fr。在本例中,这意味着“占用剩余空间”。由于我们的网格没有设置高度,因此这将成为我们想要显示的元素的总高度。

现在,我们终于有了制作动画的部分!

你看,即使0fr元素的高度已经计算出来了(这与的不同max-height),所以浏览器知道要动画到什么高度......这就是所有这些通过网格、分数和模板行跳转的原因!

我们现在可以制作动画了grid-template-rows

所以为了完善,我们需要应用一个transitionCSS 属性。在这个例子中,值为 1 秒。

transition: grid-template-rows 1s;
Enter fullscreen mode Exit fullscreen mode

把它们放在一起!

因此,通过结合visibility-hiddentemplate-rows我们transition就有了一个可以在任何高度工作的开口部分!

一探究竟!

您可能已经注意到了这里有一个细微差别:has

这就像说“如果里面的项目:has出现在这个元素里面,那么选择这个项目”

我们需要这个,这样我们就可以grid-template-rows轻松地改变父级。

.grid-rows:has(p:target)grid-rows实际上是说“如果里面有一个<p>元素与选择器匹配:target(我们在本文前面讨论过),则选择具有该类的元素。””。

这样,我们可以grid-template-rows在需要时切换我们的属性。

,内容太多了,现在让我们看看一些小窍门,让它变得更有趣!

滚动条仅在动画后显示

由于这是一个黑客行为,我不会讲得太详细,但我遇到了一个问题。

在最后的演示中,我限制了内容部分的高度(以便下一个手风琴部分在屏幕上可见,即使在长内容部分上,也能获得更好的用户体验)。

现在的问题是我正在为高度设置动画。所以随着部分内容的扩展,有些部分会溢出并触发滚动条。

这没什么问题,只是演示手风琴中的某些部分不需要滚动条(因为它们比我设置的最大高度短)。

这会导致一些糟糕的 UI / UX,其中滚动条在部分扩展时暂时出现,然后消失,如下面的 GIF 所示。

GIF 显示滚动条在部分展开时暂时出现,并在部分完全展开后消失

呃……讨厌它!

现在,滚动条很奇怪。如果我们一开始就让它们不可见(overflow-y: hidden),然后动画化它们变为可见(overflow-y: auto),动画结束后它们会再次消失。

所以,我破解了它!

我设置了一个很长的动画持续时间,并在动画的早期就使它们可见。

animation-duration: 1000s;
animation-name: show-scroll; 

@keyframes show-scroll {
    0% {
        overflow-y: hidden;
    }
    0.2% {
        overflow-y: auto;
    }
    to {
        overflow-y: auto;
    }
}

Enter fullscreen mode Exit fullscreen mode

这里我们的动画持续了 1000 秒,但在动画的 0.2%(2 秒)时改变为最终状态。

因此,16 分 40 秒后,此滚动条将消失,但我认为这在这里是可以接受的(至少通过黑客攻击尽可能可以接受,哈哈)。

我确信有办法实现这一点,但有时你只需要将 95% 足够好的东西交付生产!

现在我们得到了最初隐藏的滚动条,然后在扩展动画完成后出现。

最后一点:如果有人打开页面 17 分钟后滚动条消失了,当他们再次展开该部分时,滚动条会重新出现,并且该部分仍然可以滚动。这就是为什么我说这个解决方案“足够好”,因为这种情况很少发生,而且它会在交互时自动修复!

prefers-reduced-motion简单的方法

在我之前关于手风琴的文章中我prefers-reduced-motion也进行了介绍。

这意味着,对于那些需要减少页面运动的人(例如,由于前庭功能障碍而可能因运动而头晕或生病的人)来说,他们可以有办法向我们表明他们希望减少动画。

在上一篇文章中,我使用了 JavaScript 来检查这个媒体查询。但由于prefers-reduced-motion它主要为 CSS 设计,所以在 CSS 中执行起来要容易得多。

所以我们要做的是:

  • 默认动画
  • 如果有人prefers-reduced-motion: reduce在其浏览器中将其设置为首选项,则减少或删除该动画。

现在这里有一个“技巧”可以让这一切变得简单。

将动画持续时间设置为 CSS 属性

这样,我们只需要在一个地方切换动画持续时间,就可以在多个地方更新它。

像这样:

/* the ":root" element is essentially the "top level" or "global" place where you can define CSS properties etc.
:root {
    --open-duration: 0.5s;
}

/* we use our CSS var in place of a static duration. 
.some-item-to-animate{
  transition: grid-template-rows var(--open-duration);
}


/* we can use that same duration in multiple places
.some-other-item-to-animate{
  transition: grid-template-rows var(--open-duration);
}

/* we check for `prefers-reduced-motion` with a @media query and update the CSS variable if it matches.
@media (prefers-reduced-motion) {
    :root {
        --open-duration: 0s;
    }
}

Enter fullscreen mode Exit fullscreen mode

通过将动画设置为“0s”即可有效关闭动画!

附注/提示

通过使用,calc您实际上可以从单个 CSS 变量为整个站点的动画创建切换。

我不会完整解释这一点,因为它超出了本文的范围,但以下 CSS 可能会帮助您了解它是如何工作的!

:root {
    --animations-on: 1; /* 1 = yes, 0 = no, we can toggle this with a media query like the previous example */
}

/* animation takes 2 seconds (assuming --animations-on = 1) */
.some-element-with-animations{
    --duration: 2s; /* local animation duration */
    transition: [item to transition] 
                calc(var(--duration) * var(--animations-on));
}

/* animation takes 4 seconds (assuming --animations-on = 1) */
.some-element-with-animations{
    --duration: 4s; /* local animation duration */
    transition: [item to transition] 
                calc(var(--duration) * var(--animations-on));
}
Enter fullscreen mode Exit fullscreen mode

希望这个小提示可以给你一些关于如何允许用户设置等的有趣的想法。

展开所有打印部分@media print

另一个媒体查询技巧/窍门...以及一个可以与手风琴模式很好地配合使用的技巧,<details>以及<summary>我在上一篇文章中分享的技巧/窍门。

仅当有人打印页面时,媒体@media print查询才会应用样式(这包括“打印为 PDF”,以防您认为没有人会打印您的页面,哈哈!)

我们需要做的就是将“打开状态”CSS应用到.content演示中的每个部分

@media print {
    /* our parent item */
    .tota11y-accordion > li { 
        grid-template-rows: min-content 1fr;
    }

    /* the item that expands */
    .tota11y-accordion > li .content { 
        visibility: visible;
        margin: 0.5rem 1rem 2rem 1rem;
        padding: 0.5rem;
    /* in our demo we set a `max-height` on elements so they are scrollable sections. We do not want that in print so we set it back to `none` so the sections can expand indefinetely */
        max-height: none;
    }
}
Enter fullscreen mode Exit fullscreen mode

阅读有关[MDN 上的媒体查询和打印媒体查询(“描述”部分中的第二项)的更多信息[ https://developer.mozilla.org/en-US/docs/Web/CSS/@media#description ]

允许用户使用复选框打开和关闭所有部分

此模式的最后一个“胜利”<details><summary>,我们可以仅使用复选框来打开和关闭所有部分。

我们应用与 CSS 查询完全相同的原则@media print,但这次将其附加到:checked复选框的状态。

最后一件事,这次我们不改变,max-height因为这次我们不想删除滚动部分!

#tota11y-open-close:checked ~ ul>li{
    grid-template-rows: min-content 1fr;
}

#tota11y-open-close:checked ~ ul>li>.content{
    visibility: visible;
}
Enter fullscreen mode Exit fullscreen mode

所以我们有一个复选框输入(<input type="checkbox" id="tota11y-open-close">) with an ID oftota11y-open-close`。

<ul>我们将其定位为包含每个手风琴部分的兄弟(在 DOM 中处于同一级别) 。

这样我们就可以使用波浪号~运算符根据:checked状态进行一些匹配(就像布尔值,checked = true,not checked = false)。

因此,我们首先检查状态,然后如果已检查,我们选择兄弟ul,以及li其中的任何内容来切换grid-template-rows(这些li是我们之前在每个手风琴部分讨论过的父项)。

我们还需要更新内容部分(即之前讨论过的子部分)。但这次注意到我们使用了>运算符吗?

这样,我们仅选择在 DOM 顺序中彼此直接相关的匹配项(任何都li必须是的直接子项,ul并且.content部分必须是的直接子项li)。

这只是为了不“污染”该.content部分(就好像我们<div class="content">在原始内容部分中添加了一个也会被选中的部分一样)。这是一个边缘情况,但它只会使其更加健壮。

最后还有一件事需要考虑。

我刚才提到了“污染”其他部分。我的意思是,很多人在 CSS 中应用的样式不够具体。由于 CSS 的“级联”(CSS 中的“C”部分),这有时会导致样式“逃逸”,并影响到不该影响的元素。

为了避免这种情况(以便您可以在自己的项目中使用这个手风琴),我已经用 限定了所有选择器的范围.tota11y-accordion

这意味着样式不能脱离具有该类的周围 div。

这是一个很有用的技巧,记住,如果你创建一个组件,请在最外层元素上添加一个类,并将其包含在所有选择器中。这将使样式的作用域保持在组件内,并使你的 CSS 更易于维护(即使它稍微冗长一些!)。

哦,对了,再快速浏览一下aria我在演示中使用的属性,并仔细阅读它们,提升你的无障碍知识永远不会有坏处!💪🏼💗

总结!

其中有很多内容,我希望您能学到一些有关 CSS 的新技巧和窍门。

正如我一开始所说,除非你绝对必须避免使用 JavaScript,否则不要在生产环境中使用这种模式。但也要记住这些模式和技巧,因为它们也许有一天会救你一命!

如果您喜欢这篇文章,请考虑留下一个💗(或者如果您慷慨的话,可以留下更多表情符号!),因为它真的很有帮助!

如果您学到了新东西,请在评论中告诉我,那太棒了!

下期再见,各位美丽的人们(还有怪物们)!💗

鏂囩珷鏉ユ簮锛�https://dev.to/grahamthedev/accessible-animated-accordion-in-pure-css-no-way-5980
PREV
你知道颜色有 4 位和 8 位十六进制代码(#11223344)吗?🤯
NEXT
10 个清洁 DEV 文章的技巧!好的,感谢您阅读!😹 🤪 🤣