减少运动以提高可访问性

2025-06-09

减少运动以提高可访问性

最初发布于a11ywithlindsey.com

嘿朋友们!在这篇文章中,我将带您了解一个较新的媒体查询(对我来说)prefers-reduced-motion:。

坦白说:我了解 CSS 的基础知识,但对新出的东西却相当落后。因为我通常关注可访问性,所以我更关注 HTML 和 JavaScript。当我关注 CSS 时,它确保了正确的颜色对比度或自定义焦点状态。有时我会使用 CSS 来实现复选框和键盘可访问性。我总是把媒体查询与响应式设计联系在一起。我从未想过媒体查询可以增强可访问性。

在这篇文章中,我们将通过以下方式更新我的博客:

  1. 添加prefers-reduced-motion查询
  2. 添加用户控制的设置以减少运动。

理解prefers-reduced-motion

对于患有前庭功能障碍的人来说,动画、缩放和平移操作可能会带来麻烦。这些功能障碍会导致晕动病和眩晕。这些不舒服的感觉你绝对不想经历,更不用说在网站上了。据我所知,前庭系统位于你的内耳,负责控制平衡。

根据vestibular.org 的数据,美国 40 岁及以上的成年人中,高达 35% 患有某种形式的前庭功能障碍。所以,这不是一个小问题。

从网络可访问性角度来看,我的主要收获是:

  1. 制作动画时要小心。
  2. 小心处理你的 gif。
  3. 使用prefers-reduced-motion
  4. 允许用户控制减少运动。

你怎么做

实现查询非常简单:

@media screen and (prefers-reduced-motion: reduce) {
  /* Reduced Motion Code */
}
Enter fullscreen mode Exit fullscreen mode

在一些地方,我的链接有一些动画。

首先,我的链接有一个底部边框,当你将鼠标悬停在它上面时,它会向下移动。

然后是我的行动号召链接,当我们将鼠标悬停在它上面时,它会放大 1.1 倍。

我和安迪·贝尔 (Andy Bell)进行了交谈,他给了我一些实施方面的建议。

@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation-play-state: paused !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}
Enter fullscreen mode Exit fullscreen mode

实施更改后,我们有了悬停效果,但没有任何过渡。

这个策略从技术上来说效果不错。不过,我想完全移除悬停效果,保留链接的下划线。我可能还会调整一下比例。

@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation-play-state: paused !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  a {
    padding-bottom: 0;
    border-bottom: none;
    text-decoration: underline;
  }
}
Enter fullscreen mode Exit fullscreen mode

经过这一改变,现在我所有的链接都只是一个简单的下划线。

带下划线的文本的屏幕截图。

如果没有过渡动画,行动号召链接在悬停时从scale(1)到会显得有点突兀。所以我把它改成了scale(1.1)scale(1.05)

@media screen and (prefers-reduced-motion: reduce) {
  * {
    animation-play-state: paused !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  a {
    padding-bottom: 0;
    border-bottom: none;
    text-decoration: underline;
  }

  .blog__more-link a {
    text-decoration: none;
  }

  .blog__more-link a:hover {
    transform: scale(1.05);
  }

  .hero__cta a {
    text-decoration: none;
  }

  .hero__cta a:hover {
    transform: scale(1.05);
  }
}
Enter fullscreen mode Exit fullscreen mode

如何在 Mac 上测试

此设置主要适用于 macOS。

  1. 前往“系统偏好设置”
  2. 前往“辅助功能”
  3. 转至显示
  4. 勾选“减少运动”

非常简单!这篇文章发布后,你就可以在我的博客上测试一下了!

创建用户控制选项以减少运动

Andy Bell 的暗黑模式帖子启发了我,让我添加了用户控制选项。我们希望用户的偏好设置优先。我们也希望考虑到那些无法使用这些设置的用户。

我们将这样做:

  1. 创建一个带有“减少运动”标签的复选框。
  2. 在我的 Gatsby 应用中添加一个checked状态和一种切换该状态的方法。
  3. 使用该状态来控制data-user-reduced-motion属性。
  4. 使用上述属性应用 CSS。
  5. 将其存储在中localStorage,以便我们保留用户设置。

创建<ReduceToggle />组件

此组件是一个带有标签的 HTML 复选框。声明一下,我使用的是class组件,而不是钩子。我有时仍然喜欢写类,而且它更利于我的思维。请关注钩子版本!

import React from 'react'

class ReduceToggle extends React.Component {
  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

我在这里唯一做的事情就是创建一个复选框输入框,并关联一个表单标签。你可能已经注意到,React 使用的是 ,而不是 for htmlFor

之后,我把它放在<Header />菜单上方的组件里。以后我会再考虑样式的优化;我知道它会破坏我的布局,但没关系。我们现在只关心功能。

添加州

我们希望继续checked向我们的构造函数添加一个状态。

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

现在我们要将该状态添加到复选框本身。

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  render() {
    const { checked } = this.state

    return (
      <div className="toggle">
        <input
          id="reduce-motion"
          type="checkbox"
          checked={checked}
        />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

接下来,我们要toggleChecked为该onChange事件添加一个方法。

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  toggleChecked = event => {
    this.setState({ checked: event.target.checked })
  }

  render() {
    const { checked } = this.state

    return (
      <div className="toggle">
        <input
          id="reduce-motion"
          type="checkbox"
          checked={checked}
          onChange={this.toggleChecked}
        />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

我总是喜欢用React Developer Tools仔细检查状态是否正常工作。具体方法如下:

  • 我检查元素
  • 转到“React”选项卡
  • 查找ReduceToggle组件
  • 确保状态正常工作!

现在我们知道状态已经生效了。让我们切换data-user-reduced-motion上的属性值documentElement。我打算在componentDidUpdate生命周期方法中添加这个操作。

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  componentDidUpdate() {
    const { checked } = this.state

    if (checked) {
      document.documentElement
        .setAttribute('data-user-reduced-motion', true)
    } else {
      document.documentElement
        .setAttribute('data-user-reduced-motion', false)
    }
  }

  toggleChecked = event => {
    this.setState({ checked: event.target.checked })
  }

  render() {
    const { checked } = this.state

    return (
      <div className="toggle">
        <input
          id="reduce-motion"
          type="checkbox"
          checked={checked}
          onChange={this.toggleChecked}
        />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

将 CSS 添加到data-user-reduced-motion

提醒一下。直接跳到 CSS 代码里复制粘贴所有内容很诱人。我建议一步一步来。我犯了一个错误,试图一次性完成所有操作,结果花了比预期更多的时间进行调试。所以,首先让我们回到我们想要的目标。

我们希望用户偏好高于系统偏好。我们将逐步增强系统偏好设置。

Gatsby 是一个静态网站生成器,所以即使 JavaScript 无法加载,我的大部分静态网站内容也应该能够加载。但是,如果 JavaScript 无法加载,我们希望在data-user-reduced-motion属性不存在时,使用系统偏好设置进行回退。因此,我们将在第一部分关于媒体查询本身的查询中添加一些内容。我们使用:not()CSS 伪类来实现这一点。

@media screen and (prefers-reduced-motion: reduce) {
  * {
  :root:not([data-user-reduced-motion]) * {
    animation-play-state: paused !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  a {
  :root:not([data-user-reduced-motion]) a {
    padding-bottom: 0;
    border-bottom: none;
    text-decoration: underline;
  }

  .blog__more-link a {
  :root:not([data-user-reduced-motion]) .blog__more-link a {
    text-decoration: none;
  }

  .blog__more-link a:hover {
  :root:not([data-user-reduced-motion]) .blog__more-link a:hover {
    transform: scale(1.05);
  }

  .hero__cta a {
  :root:not([data-user-reduced-motion]) .hero__cta a {
    text-decoration: none;
  }

  .hero__cta a:hover {
  :root:not([data-user-reduced-motion]) .hero__cta a:hover {
    transform: scale(1.05);
  }
}
Enter fullscreen mode Exit fullscreen mode

然后我们在查询之外添加 CSS,以防万一data-user-reduced-motion="true"

:root[data-user-reduced-motion='true'] * {
  animation-play-state: paused !important;
  transition: none !important;
  scroll-behavior: auto !important;
}

:root[data-user-reduced-motion='true'] a {
  padding-bottom: 0;
  border-bottom: none;
  text-decoration: underline;
}

:root[data-user-reduced-motion='true'] .blog__more-link {
  text-decoration: none;
  padding: 12px 14px;
  border: 2px solid;
}

:root[data-user-reduced-motion='true'] .blog__more-link:hover {
  transform: scale(1.05);
}

:root[data-user-reduced-motion='true'] .hero__cta__link {
  text-decoration: none;
  padding: 12px 14px;
  border: 2px solid;
}

:root[data-user-reduced-motion='true'] .hero__cta__link:hover {
  transform: scale(1.05);
}
Enter fullscreen mode Exit fullscreen mode

为了测试,我做了以下操作:

  1. 关闭 macOS 上的所有减少运动设置
  2. 取消选中“减少切换”后,确保所有动画仍然存在。
  3. 选中“减少切换复选框”并查看所有减少运动的 CSS 更改是否有效。
  4. 在元素检查器中,转到<html>文档并找到data-user-reduced-motion。删除该属性。这里我们模拟该属性从未加载过。
  5. 前往系统偏好设置,勾选“减少动画效果”。我们应该修改 CSS 来减少动画效果!

添加localStorage

现在我们已经完成了这些工作,我们想开始尝试一下localStorage。我们希望保留用户的偏好设置,以备将来使用。每次访问时都重新选择设置并不是最佳的用户体验。如果您不知道什么localStorage是最佳的用户体验,我建议您在这里暂停一下,浏览一下文档。如果您喜欢视频示例,可以观看Wes Bos 的 JS30 教程

我们要做的第一件事就是localStorage设置componentDidMount

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  componentDidMount() {
    let reduceMotionOn = localStorage.getItem('reduceMotionOn')
    console.log(reduceMotionOn)
    // if we haven't been to the site before
    // this will return null
  }

  // All other code stuff

  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

现在,我们要做的是,如果reduceMotionOn为空,则为用户创建一个默认的 localStorage 状态。我将其设置为false

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  componentDidMount() {
    let reduceMotionOn = localStorage.getItem('reduceMotionOn')

    // Just a way to get around localStorage being
    // stored as a string and not a bool
    if (typeof reduceMotionOn === 'string') {
      reduceMotionOn = JSON.parse(reduceMotionOn)
    }

    if (reduceMotionOn === null) {
      localStorage.setItem('reduceMotionOn', false)
    }
  }

  // All other code stuff

  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

挂载组件后,我要做的最后一件事是设置应用程序中的状态。我想确保我的应用程序的状态与 相同localStorage

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  componentDidMount() {
    let reduceMotionOn = localStorage.getItem('reduceMotionOn')

    if (typeof reduceMotionOn === 'string') {
      reduceMotionOn = JSON.parse(reduceMotionOn)
    }

    if (reduceMotionOn === null) {
      localStorage.setItem('reduceMotionOn', false)
    }
    this.setState({ checked: reduceMotionOn })
  }

  // All other code stuff

  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

在 Chrome 开发者工具中,前往“应用程序”>“本地存储”(在 Firefox 中,前往“存储”>“本地存储”)。然后,清除reduceMotionOn存储。刷新后,您应该会看到值为reduceMotionOnfalse。如果您前往 React 开发者工具并转到该<ReduceToggle />组件,您会发现选中状态与 reduceMotionOn localStorage 项匹配。

toggleChecked这还不是全部!我们必须在React 组件的方法中切换 localStorage 。

import React from 'react'

class ReduceToggle extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      checked: false,
    }
  }

  // All other code stuff

  toggleChecked = event => {
    localStorage.setItem('reduceMotionOn', event.target.checked)
    this.setState({ checked: event.target.checked })
  }

  render() {
    return (
      <div className="toggle">
        <input id="reduce-motion" type="checkbox" />
        <label htmlFor="reduce-motion">Reduce Motion</label>
      </div>
    )
  }
}

export default ReduceToggle
Enter fullscreen mode Exit fullscreen mode

现在,如果我选中“减少运动”并离开网站,我的用户控制偏好设置将被保留!

结论

感谢大家的参与,我逐步完善了博客的无障碍功能!希望大家在过程中有所收获。感谢 Andy 的鼓励,让我写下了这篇文章!

无论您使用什么框架,以下是本文的要点:

  1. 制作动画时要小心,并为患有前庭疾病的人提供选择。
  2. 用户控制 > 系统偏好设置
  3. 拥有渐进式增强的系统偏好设置
  4. 使用localStorage对您有利的方式,以便保留用户设置!

如果您想尝试一下,我已经为您制作了一个 CodeSandbox!

保持联系!如果你喜欢这篇文章:

  • 在Twitter上告诉我,并和你的朋友们分享这篇文章!另外,如果你有任何后续问题或想法,也欢迎随时在 Twitter 上给我留言。
  • 在Patreon上支持我!如果您喜欢我的作品,可以考虑每月捐赠 1 美元。如果您捐赠 5 美元或更高,您将可以对以后的博客文章进行投票!我每月还会为所有赞助人举办“问我任何问题”活动!
  • 抢先了解我的帖子,了解更多无障碍趣味!

干杯!祝你度过愉快的一周!

鏂囩珷鏉ユ簮锛�https://dev.to/lkopacz/reducing-motion-to-improve-accessibility-1c0i
PREV
基于 ReactJS 设计,从头构建 UI
NEXT
尽管如此,Lindsey Coded