文本截断老是出幺蛾子,那个多出来的按钮咋个藏法?

3,551字
15–23 分钟
in

搞前端的老铁们都知道,文本溢出打点(text-overflow: ellipsis)这事儿看着简单,真上手全是坑。单行截断还算老实,多行截断(line-clamp)一上,再加上一个“展开全文”的按钮,麻烦就来了。屏幕阅读器那一边,明明已经把全文读得清清楚楚,按钮却还在那里喊“点我点我”。这不是给听障辅助技术添乱么?更别提那些用aria-hidden硬藏按钮的骚操作,直接违反可访问性规范。这篇就来唠唠,怎么让那个多余的按钮既不影响视力正常的小伙伴,又不对屏幕阅读器用户造成干扰,顺便把aria-disabledaria-label的坑填平。

目录

概念拆解

text-overflow: ellipsis这玩意儿,只能管单行。多行截断得靠-webkit-line-clamp配合display: -webkit-box,比如设置-webkit-line-clamp: 3,超出三行的部分显示省略号。但截断后如果加个按钮来展开,问题就来了:屏幕阅读器(比如VoiceOver)根本不需要按钮,因为整段文字它都能读到。按钮的存在反而成了累赘,甚至会让听障用户困惑——“这按钮是干嘛的?灰的?是不是漏掉了啥?”

aria-disabled="true"是个好同志,它告诉辅助技术这按钮虽然能聚焦,但实际没啥用。可光告诉“禁用”不够,得说明为啥禁用。这时候aria-label或者aria-labelledby就得上场,给按钮补一段描述,比如“屏幕阅读器已能读到全文,此按钮无需操作”。

实操方案

下面整个活:一个卡片组件,文字超过三行时显示省略号,底部放个“显示全文”按钮,点击后高度自动展开。对于屏幕阅读器,按钮被标记为禁用并附带解释。注意整个过渡动画用了Chrome目前支持的calc-size(auto)新特性,其他浏览器降级也无伤大雅。

HTML骨架

<div class="card">
  <div class="card__text" id="articleText">
    这是一段超长的示例文本。前端路上全是坑,文本截断只是其中之一。今天聊聊怎么在保证视觉体验的同时,不让辅助技术用户感到困惑。比如这个按钮,视力正常的小伙伴点它展开全文,但屏幕阅读器其实已经能读到下面隐藏的全部内容,所以这个按钮对他们来说完全是多余的。
  </div>
  <button 
    class="card__button" 
    aria-disabled="true" 
    aria-label="此按钮已禁用,因为屏幕阅读器已自动朗读全文内容"
  >
    显示全文
  </button>
</div>

细节说明aria-disabled="true"不会真的让按钮不可点,只是告诉屏幕阅读器它处于禁用状态。aria-label里的描述要直白,别整虚的。万一项目要求按钮对所有人都是可点的(比如视力正常者需要展开),那就不能加aria-disabled,得换别的思路——不过这儿讨论的是按钮对屏幕阅读器完全多余的场景。

CSS样式

.card {
  width: 300px;
  border: 1px solid #ccc;
  padding: 1rem;
}

.card__text {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  transition: all 0.3s ease;
}

/* 展开状态 */
.card__text.expanded {
  -webkit-line-clamp: unset;
  display: block;
}

.card__button {
  margin-top: 0.5rem;
  background: #f0f0f0;
  border: 1px solid #aaa;
  cursor: pointer;
}

/* 禁用样式仅视觉反馈,不阻止点击 */
.card__button[aria-disabled="true"] {
  opacity: 0.6;
  cursor: default;
}

注意aria-disabled="true"并不阻止点击事件,得自己用JS控制按钮行为。这就是一个常见的坑:很多人以为加了这个属性按钮就不能点了,大错特错。需要额外在JS里判断并阻止点击。

JavaScript交互

const button = document.querySelector('.card__button');
const textBlock = document.querySelector('.card__text');

button.addEventListener('click', (e) => {
  // 如果按钮带有aria-disabled="true",就啥也不干
  if (button.getAttribute('aria-disabled') === 'true') {
    e.preventDefault();
    return;
  }
  textBlock.classList.toggle('expanded');
  button.textContent = textBlock.classList.contains('expanded') ? '收起全文' : '显示全文';
});

那问题来了:啥时候按钮的aria-disabled应该是false?只有在页面检测到当前没有屏幕阅读器运行时,才需要启用按钮。但前端很难完美检测屏幕阅读器,所以更稳妥的做法是:默认就给按钮加上aria-disabled="true",然后提供一个全局开关(比如设置里的“无障碍模式”),让用户自己选择是否启用视觉展开功能。这属于高级玩法,小项目可以直接按上述代码来,至少保证屏幕阅读器用户听到的是“这按钮没用”的解释,而不是被一个能点但没意义的按钮耍得团团转。

备选路子

上面用aria-label直接写描述,简单粗暴。但描述文本太长的话,维护起来略丑。另一种玩法是aria-labelledby搭配隐藏的span

HTML变形

<div class="card">
  <div class="card__text">长文本同上...</div>
  <button 
    class="card__button" 
    aria-disabled="true" 
    aria-labelledby="btnDesc"
  >
    显示全文
  </button>
  <span id="btnDesc" class="visually-hidden">
    此按钮已禁用,因为屏幕阅读器已自动朗读全文内容
  </span>
</div>

配合一个视觉隐藏类:

.visually-hidden:not(:focus):not(:active) {
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  position: absolute;
  white-space: nowrap;
}

这么做的好处是描述文本可以更复杂,甚至动态生成。坏处是多了一个DOM元素,而且aria-labelledby的优先级高于按钮内的文字内容,所以按钮上写的“显示全文”会被覆盖掉——屏幕阅读器只读#btnDesc的内容。这也许不是想要的,因为视力正常的人看到的按钮文字是“显示全文”,而听障用户听到的是“此按钮已禁用…”,信息对不上。所以更推荐第一种aria-label方案,它不影响按钮视觉文字。

终极避坑

还有一个刚进入规范但支持度不错的属性——aria-description。它专门用来给元素加补充描述,不会覆盖按钮的原有名字。写法如下:

<button 
  aria-disabled="true" 
  aria-description="屏幕阅读器已自动朗读全文,无需操作此按钮"
>
  显示全文
</button>

目前在Can I Use上显示主流浏览器都已支持(2025年后的版本),但规范还在ARIA 1.3编辑草案阶段。保守派可以观望,激进派直接上车。用aria-descriptionaria-label更语义化,因为aria-label是替换名字,而aria-description是附加描述,屏幕阅读器会先读按钮名字“显示全文”,再读描述内容。完美解决上述信息错位问题。

最后贴个完整demo的思维流程:先写HTML结构,确保按钮默认带aria-disabled="true"aria-description(或aria-label)。CSS做截断和过渡动画。JS控制点击时若检测到aria-disabled="true"return,否则执行展开。如果哪天想给所有用户都启用按钮,只需把aria-disabled改成false,同时删掉描述属性,完事。前端可访问性这条路,细节全是魔鬼,但踩平一个坑就少一个鬼。