发布于 2026-01-06 1 阅读
0

手风琴:1分钟、5分钟和10分钟版本……全部可用 [quicka11y] 由 Mux 呈现的 DEV 全球展示挑战赛:推介你的项目!

手风琴:1分钟、5分钟和10分钟版本……全部可用[quicka11y]

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

几天前我看到一篇帖子,内容是“仅使用 CSS 实现的手风琴效果”。

可惜的是,他们使用的技术并不容易掌握。而且,他们还给自己制造了太多麻烦!

不过,那篇文章至少给了我一些启发。

如果你喜欢那篇文章,那么这篇文章绝对会让你大吃一惊,我即将创作的是:

  • 一个仅使用HTML的折叠面板
  • 一个只用HTML和CSS就能实现的漂亮作品
  • 用HTML、CSS和JS制作的精美动画。

为了增加趣味性,我们将采用“1分钟、5分钟、10分钟”的形式进行!

1分钟手风琴(仅限HTML)

哇,1分钟就能造出一个手风琴,这时间真短啊!

我的意思是,我已经浪费了15秒钟说这些话了。

不过,我并不担心,在 HTML 中实现手风琴效果非常容易。

是的,我还在说话,我真的有时间。

好了,废话不多说,代码如下:

<details>
  <summary>Accordion item 1</summary>
  Accordion 1 content
</details>
<details>
  <summary>Accordion item 2</summary>
  Accordion 2 content
</details>
<details>
  <summary>Accordion item 3</summary>
  Accordion 3 content
</details>
Enter fullscreen mode Exit fullscreen mode

呼,还剩12秒!

等等,什么?那是手风琴?

你肯定知道!

一款功能齐全且非常容易上手的手风琴!

想看看它的实际效果吗?

这是:

  • 键盘操作:您可以tab逐个打开和关闭手风琴组件Enter
  • 键盘友好:它内置了焦点指示器,而且是免费的!
  • 功能强大:无需 JavaScript,即可轻松打开和关闭(并且会将状态暴露给辅助技术,使其更易于使用!),还有更多免费福利!😱

它简直太棒了,除了一个大问题……它丑爆了。呃……我们来改造一下吧!

5分钟手风琴(HTML和CSS)

好了,我们现在有了基础的HTML代码,接下来我们可以用它做什么呢?

幸运的是,这些<summary>元素<detail>很容易搭配!

这里 HTML 中唯一改变的是,我们为<div>所有隐藏的内容添加了一个 <div> 标签,以便更容易定位它。

open哦,我还给第二个元素添加了一个属性“ ”,来展示该<details>元素的另一个技巧!一个属性就能设置打开状态!

经过几分钟的样式设计,我们只用 HTML 和 CSS 就得到了一个(在我看来)很吸引人的手风琴效果图。

唯一不寻常的地方是:

details > summary::marker,
details > summary::-webkit-details-marker {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode

这样我们就可以隐藏默认的打开和关闭标记,以便我们可以使用 SVG 背景图像(在本例中)创建自定义标记。

看起来不错……是的,你很棒,手风琴看起来也不错!

现在我们要把它弄得花里胡哨的吗?

10分钟手风琴(HTML、CSS和JS)

<details>纯 HTML 和 CSS 手风琴效果很棒,但如果在打开和关闭组成手风琴的每个元素时能有一个柔和的动画效果,那就更好了。

幸运的是,借助一些 JavaScript 技巧,我们可以实现这一点。

这是我编写的 JS 代码(部分内容是抄袭的!嘿,我只有 5 分钟时间来添加 JS 功能……饶了我吧!)。

以下代码包含一些关键点的解释:

class Accordion {
  constructor(el) {
    this.el = el;
    this.summary = el.querySelector('summary');
    this.content = el.querySelector('.content');
    this.animation = null;
    this.isClosing = false;
    this.isExpanding = false;
    this.summary.addEventListener('click', (e) => this.onClick(e));
  }

  onClick(e) {
    e.preventDefault();
    this.el.style.overflow = 'hidden';
    if (this.isClosing || !this.el.open) {
      this.open();
    } else if (this.isExpanding || this.el.open) {
      this.shrink();
    }
  }

  shrink() {
    this.isClosing = true;
    //we need to grab the height of the summary and the overall element before we shrink it.
    const startHeight = `${this.el.offsetHeight}px`;
    const endHeight = `${this.summary.offsetHeight}px`;
    //stop previous animations.
    if (this.animation) {
      this.animation.cancel();
    }

    //start the new animation, starting at full height and reducing down to the minimum
    this.animation = this.el.animate({
      height: [startHeight, endHeight]
    }, {
      duration: 400,
      easing: 'ease-out'
    });


    this.animation.onfinish = () => this.onAnimationFinish(false);
    this.animation.oncancel = () => this.isClosing = false;
  }

  open() {
    //important step, we grab the current height and set it, this could be mid animation so we need to set it each time.
    this.el.style.height = `${this.el.offsetHeight}px`;
    this.el.open = true;
    window.requestAnimationFrame(() => this.expand());
  }

  expand() {
    this.isExpanding = true;
    const startHeight = `${this.el.offsetHeight}px`;
    const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;

    if (this.animation) {
      this.animation.cancel();
    }

    this.animation = this.el.animate({
      height: [startHeight, endHeight]
    }, {
      duration: 400,
      easing: 'ease-out'
    });
    this.animation.onfinish = () => this.onAnimationFinish(true);
    this.animation.oncancel = () => this.isExpanding = false;
  }

  onAnimationFinish(open) {
    this.el.open = open;
    this.animation = null;
    this.isClosing = false;
    this.isExpanding = false;
    this.el.style.height = this.el.style.overflow = '';
  }
}

document.querySelectorAll('details').forEach((el) => {
  new Accordion(el);
});

Enter fullscreen mode Exit fullscreen mode

把所有部件组合在一起,我们就得到了这架漂亮的手风琴:

好了,这就是1分钟、5分钟和10分钟的手风琴演奏。但我们还没结束呢!

摘要<summary>

是的……我不得不这样命名这一部分,我很抱歉(但并不抱歉!)😂。

如你所见,只需短短几分钟,我们就能制作出一个漂亮的手风琴。

我承认,纯CSS部分花了大约7分钟,JS部分花了大约15分钟……但我编程速度比较慢,好吗?

无论如何,我们都创造了一种易于使用且易于维护的手风琴。

不,我们还没完呢……让我们再提升一个档次!💪🏼

额外复制粘贴组件版本

作为对您耐心看到这里的额外奖励,我们特地将手风琴组件重新设计,使其成为您可以在网站上重复使用的组件。(这是超级秘密的“一小时速成版”!😂)

我还添加了一个可选参数,允许您设置当打开一个部分时,其他部分自动关闭!

此外,我还添加了一个重要的功能:我们测试prefers-reduced-motion

prefers-reduced-motion

在前一个例子中,我们对打开和关闭都添加了动画效果。

现在,动画的问题在于,对于某些人来说,它们可能会令人不安,或者导致他们感到恶心或头晕。

因此,浏览器为我们提供了一个媒体查询,我们可以使用该查询来检查该用户是否偏好减少动态效果。

通过在我们的 JS 中查询该媒体查询,我们可以激活或移除eventListeners负责动画的元素,并尊重它们的偏好/需求。

您可以prefers-reduced-motion在 MDN 上阅读更多相关内容。

那么,考虑到这一点,让我们来看看如何使用该组件及其选项吧!

如何使用该组件!

你只需要这样做:

  • 获取 HTML 代码,并将其中的<summary>内容以及该部分内部的内容替换<div class="contents">成你自己的内容!(务必获取<div>带有类名的外部元素tota11y-accordion,否则将无法正常工作!)
  • 请包含 CSS 和 JS!
  • 添加您想要设置的任何选项(请参阅 CodePen 之后的章节)。

可配置选项

您可以在 JS 中更改几个选项。


// create a new Tota11yAccordion, passing the wrapper element and then the options.
new Tota11yAccordion(el,{
      // close other items in the accordion when one is opened.
      closeOthers: true, // default false

      // set the duration of the animation
      duration: 800, // default 400

      // set the animation easing
      easing: 'ease-in' // default ease-out
  });
Enter fullscreen mode Exit fullscreen mode

此外,我还添加了一些样式选项作为 CSS:root元素的变量,这样您就可以在那里进行一些样式设置,而无需翻遍所有内容。

最后需要说明的是,我给所有 CSS 都添加了前缀,.tota11y-accordion这样样式就不会影响到页面的其他部分。

最后想说几句!

如您所见,这些<details>元素<summary>功能强大,而且使用和样式也很简单。

而且,最重要的是,它们很容易获得

比其他手风琴式组件更容易创建,而且美观又包容?这就是以无障碍设计为先的魅力所在!🚀

谈到无障碍设计

你听说过我的公司totally.dev吗?

如果不是,您是否拥有 SaaS 产品或管理开发团队,并希望学习如何使您的应用程序易于访问?

你想了解如何开拓超过 10 亿残疾人士的新用户群体,让他们使用你的产品吗?

如果这听起来很有趣,那就去看看totally.dev吧!

我们提供无障碍即服务(没错,它的缩写是“AaaS”……没错,我们在营销中也大力宣传这一点!😂),让您能够将您的产品和团队从无障碍零基础提升到无障碍英雄级别!

请看看我的AaaS产品……如果你喜欢的话请告诉我!(看……我们可是全力以赴的,哈哈)。

您是一名想要了解无障碍设计的开发人员吗?

我正在直播讲解无障碍设计,并为感兴趣的人提供一些辅导。如果你想参与,请在推特上私信我,我的账号是GrahamTheDev,我非常欢迎你加入#A11y100 💪🏼💗!

文章来源:https://dev.to/tota11ydev/accordions-1-5-and-10-minute-versionsall-accessible-quicka11y-2d3b