让我们谈谈状态。指的是向用户传达状态,而不是应用程序在 JavaScript 对象或 localStorage 中存储状态。我们将讨论如何让用户了解状态(例如,按钮是否被禁用,或者面板是否处于活动状态),以及如何使用 CSS 来实现。出于即将阐明的原因,我们不会使用内联样式,或者尽可能避免使用类选择器。
还在看?不错。让我们开始吧。
应用程序的所有动态组件都具有默认的面向用户的状态,并且需要随着用户与这些组件交互而存储和更新该状态。
例如,当按下按钮时,会发生一些事情(这就是按钮的用途)。当这些事情发生时,它们通常以界面中的视觉方式表示。按钮的背景可能会更改以指示它已被按下。如果按钮控制界面中的其他组件,则这些组件的样式可能会发生视觉变化,或者在某些情况下会切换其可见性。项目被删除,弹出通知,应用错误样式等。
您可能已经注意到,我们一直在多次提及组件的“视觉”状态。这正是我在许多教程、文章和关于状态的一般讨论中发现的问题类型。
通常,开发人员使用“有状态”类来管理组件的状态。但这非常不足,因为组件不仅仅由其外观组成。需要与组件的视觉表示一起管理底层语义。一旦您通过键盘和/或屏幕阅读器与之交互,就会发现未能管理这些底层语义的问题。
本文重点介绍如何适当地传达状态,以便除视力正常、使用鼠标的用户之外,所有用户都可以与我们的界面进行交互。
状态不仅仅是外观
除了使用 CSS 适当地隐藏内容 以便于视力正常用户和辅助技术之外,CSS 对元素的语义或可访问状态没有太多有意影响。我的意思是,除了像 不受支持的“speak”、before/after 伪内容以及根据用户偏好专门重新设置组件样式的媒体查询(如 减少运动媒体查询 和其他提议的 用户查询)之外,CSS 本身并非旨在更改元素的语义、内容或以有意义的方式适当地传达元素的状态。
我为什么要提起所有这些?因为仅使用 CSS 类来管理状态对于向所有用户传达状态来说,大多数情况下都是不足的。作为一种用于展示目的的语言,为输入赋予 .has-error
类以将边框颜色更改为红色阴影,对它没有任何语义价值。就 CSS 而言,“这就是您想要设置该输入的样式。不错。我支持您!只是不要要求我在 DOM 中向上设置样式。我在这里划清界限,伙计……”
相反,为了管理和传达状态,我们应该更新相应元素上的属性。不,我指的不是 data 属性。这些也没有任何意义。如果我们采用这种方法,在许多情况下,我们甚至不需要有状态的类,除了切换元素 display
的类之外。
我们是否忘记了可以使用属性选择器设置样式?
HTML 和 ARIA 有一系列属性,应该用于适当地传达组件的当前状态。
考虑在您的 <button></button>
或 <input type="text" />
上使用 .is-disabled
类?这只会视觉上禁用它。您仍然必须以编程方式关闭该元素的点击和键盘事件。相反,使用 [disabled]
属性,您将拥有一个用于设置元素样式的 CSS 选择器,浏览器将为您完成所有适当的工作以禁用该元素!
所以,不要使用
input.is-disabled {
opacity: .65;
}
而是使用
input[disabled] {
opacity: .65;
}
这与使用 .is-disabled
类实现了相同的视觉效果,但我们利用了我们需要设置的属性的属性选择器,以便向浏览器和用户传达当前状态。同时,如果我们只是切换类,则无需执行上述任何额外工作(使用 JavaScript)来禁用输入。
示例:处于“活动”状态
为了提供一些更深入的上下文,让我们来看一个您可能使用 .is-active
类的场景。对于不同的组件,“活动”可能意味着完全不同的东西,这就是为什么我可以理解想要使用单个可重用类名,而不是确定需要管理哪个属性以适当地传达状态。但使开发人员更容易管理状态并不一定会帮助用户,所以让我们以正确的方式来做。
活动导航链接
首先,让我们看看如何在导航中声明当前活动的链接。以下 Pen 包含两个示例。第一个使用 .is-active
类来指示当前导航项。第二个使用 aria-current="page"
。
查看 Pen .is-active 与 aria-current=’page’,作者:Scott (@scottohara),来自 CodePen。
虽然它们看起来完全相同,但如果您使用 Jaws 18、Voice Over 或 NVDA 2017.2(发布时)导航示例,您会听到类似以下内容:“功能,当前页面。”当使用 aria-current
与示例交互时。查看 Léonie Watson 关于 [aria-current]
的文章,了解可以在其中使用此属性进行样式设置(代替 .is-active
类)的其他许多示例。
活动按钮
根据按钮的目的,可能需要通过以下 ARIA 属性之一来增强按钮的活动状态,以供屏幕阅读器用户使用
aria-expanded
– 指示按钮控制界面中的另一个组件,并传递该组件的当前状态。aria-pressed
– 指示按钮的行为类似于复选框,因为它将其状态在按下或未按下之间切换。
如果不使用上述属性之一,则按钮没有内在的方式来传达它是否已被交互过。如果某种情况不需要这样做,那完全没问题,但如果您确实需要传达按钮已被激活,则可以使用 aria-pressed
来实现。
查看 Pen 切换按钮示例,作者:Scott (@scottohara),来自 CodePen。
在上面的示例中,我们有一个可以与之交互的按钮,用于将项目添加到购物车。为了指示何时添加了项目,我们没有使用类,而是切换 aria-pressed
属性的布尔值,并使用 [aria-pressed="true"]
作为我们的样式钩子来视觉上传达活动状态。当通过屏幕阅读器与按钮交互时,它将被宣布为“已选中”或“未选中”、添加到购物车、切换按钮。
要深入了解在开发可访问的切换按钮时应考虑的事项,只需查看 Heydon Pickering 的切换按钮文章 即可。Heydon 详细概述了为什么更改按钮的可见标签不是一个好主意,甚至指出了您可能实际上并不想要切换按钮,而是应该考虑使用 开关。
管理手风琴状态
对于我们的最后一个示例,让我们看看如何在手风琴组件中管理状态
查看 Pen ARIA 手风琴示例,作者:Scott (@scottohara),来自 CodePen。
如果你仔细阅读 CSS 和 JavaScript 中的注释,你会注意到这个演示做了一些事情。
首先,手风琴的标记模式的构建方式使得即使 JavaScript 因任何原因被禁用,用户也不会无法访问任何内容,因为只有在存在 .js
类的情况下,面板才会被隐藏。
其次,为了避免在每个手风琴面板标题中使用实际的 <button></button>
元素,我们改为将它们嵌套的 <a>
转换为“按钮”,方法是应用 ARIA role="button"
,然后通过键盘按下事件监听器添加所有预期的键盘功能。此外,为了确保键盘用户可以访问“按钮”,已将 tabindex="0"
设置为每个 ARIA 按钮。
最后,我们在这里使用 aria-expanded
属性来传达手风琴面板的当前状态,因此当用户使用屏幕阅读器将焦点放在手风琴触发器上时,它会宣布“手风琴标题,已折叠(或展开)按钮”。
你会注意到手风琴面板正在利用 .is-active
类来切换其可见状态。天哪!但是等等,这是我们**可以**依靠 CSS 独自帮助我们的一件事。如果我们仔细看看这里使用的选择器
.js .accordion__panel {
border-bottom: 1px solid;
overflow: hidden;
padding: 0 1em;
max-height: 0px;
transition:
max-height .2s ease-in-out,
visibility .2s ease-in-out;
visibility: hidden;
}
.js .accordion__panel.is-active {
max-height: 100vh;
visibility: visible;
}
第一个选择器,即依赖于 JavaScript 可用的那个,利用 visibility: hidden
包容性地隐藏面板的内容,使其对有视力用户和辅助技术用户都不可见。然后设置溢出、最大高度和过渡属性以折叠面板,并在 .is-active
类添加到手风琴面板后,准备将其扩展为展开形式。我本可以切换 display: none
或以编程方式添加和删除面板的 hidden
属性,但这样会失去面板打开过渡的能力。而且每个人都喜欢好的过渡,对吧?
结束语
我主要希望你从所有这些中获得的启示是,如果你只是切换类来直观地管理组件的状态,那么你可能没有适当地向辅助技术用户传达该状态。
你需要使用合适的元素(<button></button>
是你的朋友!),并管理合适的属性及其值,以创建真正可访问的用户体验。当然,你可以执行这些操作并继续切换状态类来控制你的样式。但是,如果我们必须更新属性及其值,并且这些也是有效的 CSS 选择器,那么为什么要通过切换类来做更多不必要的工作呢?
很棒的文章,Scott。
我正在审查我们的一堆可访问性代码,尤其是手风琴、选项卡、导航栏等内容,因此将认真考虑删除“is-state”类,并改用适当的属性选择器。
Heydon Pickering 的文章为切换按钮 POC 奠定了基础——http://codepen.io/basherkev/pen/jmZpyv——我现在已将其纳入我们的 UI 组件库。
也很高兴最近看到如此多的优秀 A11Y 文章和资源出现。
谢谢!
很棒的信息,我是一名 Web 开发人员,我们公司正在采取重大措施,确保我们所有的服务/网站都符合 WCAG 2.0 AA 标准。阅读本文后,我结合使用了 aria-controls 和 aria-expanded 来处理我们的导航交互。(以前使用的是类)
菜单不仅现在更易于访问,而且其背后的代码也更具语义性和易于理解。
太棒了,Shane!很高兴它对您有所帮助。
我喜欢这篇文章的想法,我认为它表达得非常好,但我有一个小小的异议。这篇文章的前提似乎是
问题在于,CSS 类是迄今为止最可扩展的样式化方法。你熟悉 SMACCS 吗?
https://smacss.com/
虽然我同意我们都应该努力为状态设置正确的属性,但我认为也值得切换类,以便在大型项目中维护可扩展的样式表。
嘿,Scott,
虽然你在我提倡利用我们可以在组件上适当地更新用户界面状态时可用的属性选择器方面部分正确,但我认为这篇文章的前提实际上是“以可访问的方式构建事物,你将免费获得许多状态化样式钩子”。正如我在结束语中总结的那样:既然我们免费获得了这些钩子,为什么还要额外做一些工作,也要引入额外的类和 JS 并管理它呢?
至于 SMACCS,我非常熟悉它。我欣赏并喜欢它背后的许多想法,但我也没有完全接受它,或者任何单一的 CSS 方法。我个人认为 CSS 是一种功能强大的语言,有很多东西可以提供,不应该局限于一种选择器“因为它最具可扩展性”。不同的选择器是有原因的,如果我们共同更好地利用它们,那么也许许多这些“CSS 已损坏”的讨论实际上会自行解决 :)
但是,我老实说在以下方面没有(可扩展的)区别
.tab-is-open { /* 来自 smacss 网站 */ }
对比
.tab[aria-expanded=”true”] {}
前者无助于教育初级开发人员,除了简单地交换一些颜色之外,还有更多方法可以传达状态。而后者应该让他们思考“这是什么?我应该了解这是什么意思。”
无论如何,我可以在此评论中写一篇关于此主题的完整子文章,但我宁愿和你谈谈它。因此,这里公开邀请你联系我,如果你愿意,我们可以继续讨论。
谢谢 :)
那么,SMACCS 为什么有价值?两个原因。语义选择器和弱选择器。我同意你在本文中的方法非常语义化。赞扬。但是基于类的选择器足够语义化,并且更弱。在你引用的示例中,
.tab-is-open { /* 来自 smacss 网站 */ }
与.tab[aria-expanded=”true”] {}
的区别在于可扩展性是显而易见的:SMACCS 版本“两倍”可扩展,因为它只有一半强度(以我的虚构可扩展性……嗯……规模为依据)。重点是,SMACCS 使用单个类名,而你使用类和属性。你离!important
的路径更远了,那里存在疯狂。在大型项目中,保持选择器弱是首要考虑因素之一。我不会武断地说它是最重要的考虑因素,但它绝对很重要。
嘿,Scott,
你的回复没有引用 SMACCS 网站关于状态类的那一部分
“由于状态可能需要覆盖更复杂的规则集的样式,因此允许使用 !important,而且我敢说,建议使用它。(我以前说过 !important 从未被需要过,但在复杂的系统中,它通常是必需的。)”
根据你对我的示例的批评,你试图在那里扩展什么?为什么除了原始的 '.tab' 元素之外,还要在任何其他元素上使用 'tab-is-open' 类?如果该状态类不应该在任何其他组件上使用,并且它应该覆盖选项卡的默认样式,那么使用更严格的选择器在这里为什么是一个糟糕的选择?
即使是更通用的类似 'reusable' 的 is-open 类。它在什么上下文中使用?选项卡、屏幕外菜单、工具提示、手风琴?我认为每个的样式都需要根据它们是否需要 CSS 过渡或内容的原始状态是否真正对所有用户隐藏,或者内容是否只是视觉隐藏(考虑需要单击事件才能打开的下拉菜单,与应在悬停/聚焦主触发器或任何下拉菜单子元素时打开的下拉菜单)而有所不同。
那么,一个除了名称之外实际上不可重用的可重用类有什么可扩展性?名称识别?改为熟悉 [disabled] 和 [aria-expanded] 等属性选择器,因为它们确实有意义,并且经常被忽视,而被状态类所取代。
最后:“但是基于类的选择器足够语义化……”
如你所知,我不同意这种观点。我认为开发人员以智能、可访问的方式构建事物并利用我们所有可用的工具来做到这一点更为重要。
再次感谢你的回复,因为这些是我在文章中没有深入探讨的主题,因为我不想让人误以为我在贬低 SMACCS。再次强调,我认为 SMACCS 总体上是一种优秀的 CSS 方法……但我可以这样认为,并且仍然对其中一个方面提出异议,而是提倡以一种实现类似样式效果的方式来做,但使用背后具有实际语义的选择器。正如我在结束语中提到的,如果你愿意,你可以继续使用状态类,以及更新组件的属性以真正传达状态。我个人认为你只是因此做了额外的工作。
关于 !important。是的。我也同意这一点。但是我用这段话要表达的是,状态类应该更具体。你专注于 !important 部分,而忽略了在覆盖默认状态时使用更强选择器是可以的这一事实。
关于你的反例,很难反驳这种模糊的场景,因为虽然你说它不够用,但我想到过一些类似的场景,在那里它运行得很好,但要在这里充分地讨论它,需要更多关于标记元素的行为和用户对交互的期望的上下文。
我很高兴我们没有争论属性的管理,因为这是这里最重要的事情。但既然我已经承认我不严格遵循 SMACSS,我不确定继续这个讨论有什么意义?你不同意我对 CSS 选择器的观点,这很好。你同意管理类和属性都需要更多前期工作。好的……
看来我们只是在争论 CSS 方法论,这不会有什么结果。因为对于你给我的每一个例子,我都可以继续举出例子来支持我的立场。
因此,我真诚地对你说,保重,Scott,感谢你的讨论 :)
touché,他确实承认
!important
不是最糟糕的情况,但下一句话是,“除非你真正需要,否则不要使用 !important”。如果你将弱选择器作为首要任务,你就不会需要它。你关于 tab-is-open 场景的例子让我愣了一下,因为我同意,乍一看,你的属性选择器似乎永远不需要被覆盖,这使得选择器强度变得无关紧要。但是,我现在的工作中有一个我认为很常见的反例:一行标签,其中大多数触发下拉菜单,但一个触发搜索框,并且在这样做时它们会获得非常不同的样式。在我看来,仅使用属性选择器在这里是不够的。你需要通过某种类的样式来设置样式,并且通过包含属性选择器,你正在不必要地增加选择器的强度。
我当然没有反对将你的属性管理为状态更改,我只是根据个人经验认为管理类也是值得的。你称之为额外的工作,并且在前期它确实如此。但随着项目的年龄增长和发展,它会带来回报,你可以愉快地添加或更改模块,而不用陷入选择器大战。
很好,感谢你的聊天和发人深省的文章。
我建议使用 summary 和 Details 来实现手风琴 - 就像所有其他 HTML 元素一样,它们具有内置的可访问性 - 我知道 MS 仍然不支持它们。但另一方面,回退是有效的(只是盒子)。此外,还有 polyfill - 我推荐 https://mathiasbynens.be/notes/html5-details-jquery
它们甚至附带了“open”属性,你可以将其用于样式和进一步的 JS 改进。无论是否使用 Js:summary 和 details 都是你想要用于可扩展内容的元素,就像应该用于按钮或 hx 用于标题一样。总是!
嗨,Marc,
关于 details/summary 用法的建议很好。如果内容决定了那是要使用的适当标记模式,那么它绝对应该是首选,当然也要使用 polyfill。
我想专注于 ARIA 手风琴,因为它在我的论点中更有意义,即使用 JavaScript 切换属性而不是类。details/summary 不需要(不应该)任何 JavaScript 或类切换,因此,虽然它可能是一个合适的组件,但它并没有真正解决我想强调的 JS 组件无法正确传达状态的问题。
希望这能解答你可能对为什么我选择朝那个方向发展提出的任何疑问。
谢谢!
你说得对 - 我没有考虑到你想解释什么,当然我知道,有些项目需求使得有必要使用除了 details/summary 之外的其他元素 - 有时更有意义。此外,details/summary 还有一个重要的缺点:在我看来,它更多的是一种表现性标记而不是语义标记。我实际上经常想使用 article/hx 或其他东西而不是 details/summary。
无论如何,在我看来,一些开发人员只是忘记了这些元素 - 也许很长一段时间以来浏览器支持不佳,开发人员对用于手风琴的 jquery 插件过于熟悉,这应该是一种解决方法,但成为了他们工具箱中舒适的一部分。
因此,我认为一次又一次地提及 details/summary 是一个好主意 - 特别是在涉及包容性设计时。 - 如果你想构建一个只允许打开一个细节的手风琴,那么无论如何都需要 JS。但同样,使用(和操作)open 属性非常方便。
希望这能帮助一些人清理一下他们的工具箱 ;-)