进步博客

制作无障碍的星型评级控件

原文:Developing an Accessible Star Ratings Widget

作者:Thierry Koblentz

先睹为快,跳到演示页

许多电子商务网站、社交网络服务以及网上社区都包括评级或评估的功能。征集用户的意见已经成为一种商业模式,现在有许多网站致力于评价产品、服务、企业或其他。

最常见的显示投票的界面是“星级系统”,每个评论者给评审项目赋予一定数量的分值(通常以星表示)。很多网站都使用了这种模式,无论Amazon还是Yelp。

图A     Amazon 和 Yelp 的星型评级

正如图A所示,两者的视觉界面类似,但使得这两种方案变得有趣的是它们使用的基本标记。一个使用的是<map> ,另一个是<img> 。

你也许会认为大多数评级系统所使用的标记是具有语义的,并在多种用户代理下都可以操作 – 也就是说,这种评级系统应当基于一组特定的HTML元素和属性集来构造,使用JS和CSS来控制行为和样式。如果是这样的话就好了,但事实远非如此。作者使用的标签可谓是千奇百怪:

微格式(Microformats)案例

在介绍几个基于图像的标记评级的技术之前,我认为值得提一下一个基本的和直接的方法( 来自微格式 ),使用字符 :

<abbr class="rating" title="3 stars">***</abbr>

优点

缺点

注 :我选用“*”,而不是★(★),因为屏幕阅读器(至少JAWSNVDA )似乎忽略HTML实体。

基于图片的评级标记

当涉及到显示图片,作者有很多选择。

每一个级别使用一张图片

使用一张图片:

<img src="4stars.png" alt="4 out of five">
一星
两星
三星
四星
五星

优点

缺点

一组使用一张图片

来自whatwg的工作草案

<img alt="4 out of 5" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="one-star.png">
<img alt="" src="no-star.png">
一星
二星
三星
四星
五星

优点

缺点

请注意这个例子来自一个有争议的工作草案 。在我看来,这种方法是不能接受的,因为替代文字没有准确、简明地描述图像。此外,如果这种方法的依据是这些图片代表内容,那么为什么不给他们添加替代文本呢?

Ajaxian为例,作者为每一张图片都添加了替代文本,如果他认为每张图片都是内容的话,这将很有意义 :

<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="+" src="star1.png"/>
<img [snip] alt="-" src="star0.png"/>
<img [snip] alt="-" src="star0.png"/>

在任何情况下,使用多张图片与使用单一元素(img或者其他)相比,具有便于投票的优点——用户选择其中一颗星便完成投票。因此,我们应该记住这一点…

背景图片sprite

下面这个技术改编自一个技巧,最初由Yahoo! Music的开发人员实现:

标记

<span class="rating r1 stars">1 of 5</span>
<span class="rating r2 stars">2 of 5</span>
<span class="rating r3 stars">3 of 5</span>
<span class="rating r4 stars">4 of 5</span>
<span class="rating r5 stars">5 of 5</span>

CSS

.stars {
  background: transparent url(img/sprite.png) no-repeat;
}
.rating {
  font-size: 0;
  height: 19px;
  overflow: hidden;
  vertical-align: middle;
  width: 96px;
  display: block;
}
.r1 { background-position: -385px 0; }
.r2 { background-position: -288px 0; }
.r3 { background-position: -192px 0; }
.r4 { background-position: -96px 0; }

译者注:sprite图

优点

缺点

标记中的sprite

这种方法是基于TIP方法 ,即用一个sprite图片作为<img>元素而不是背景图片:

标记

<span title="1 of 5" class="rating r1"><img width="0" height="1" src="sprite.gif" alt=""/>1 out of 5</span>
<span title="2 of 5" class="rating r2"><img width="0" height="1" src="sprite.gif" alt=""/>2 out of 5</span>
<span title="3 of 5" class="rating r3"><img width="0" height="1" src="sprite.gif" alt=""/>3 out of 5</span>
<span title="4 of 5" class="rating r4"><img width="0" height="1" src="sprite.gif" alt=""/>4 out of 5</span>
<span title="5 of 5" class="rating r5"><img width="0" height="1" src="sprite.gif" alt=""/>5 out of 5</span>

CSS

.rating {
  position: relative;
  height: 1.6em;
  width: 8.1em;
  overflow: hidden;
  vertical-align: middle;
  display: block;
}
.rating img {
  position: absolute;
  width: 40.5em;
  height: 1.55em;
  top: 0;
  border: 1px solid #fff;
}
.r1 img { right: 0; }
.r2 img { left: -24.4em; }
.r3 img { left: -16.2em; }
.r4 img { left: -8.1em; }

优点

缺点

值得注意的是,它不像其他的图片替换技术,此方法允许:

投票标记

机制

为了投票 ,我们需要一个低级别的投票机制,允许普通用户选择和提交。为此,我们可以使用表单:

标记

<fieldset>
  <legend>Rating</legend>
  <label><input type="radio" name="movie" value="1_5">1/5</label>
  <label><input type="radio" name="movie" value="2_5">2/5</label>
  <label><input type="radio" name="movie" value="3_5">3/5</label>
  <label><input type="radio" name="movie" value="4_5">4/5</label>
  <label><input type="radio" name="movie" value="5_5">5/5</label>
</fieldset>

结果

为了更好的可读性,我们添加<br>和空格。

标记

<fieldset>
  <legend>Rating</legend>
  <label><input type="radio" name="movie" value="1_5"> 1/5</label><br>
  <label><input type="radio" name="movie" value="2_5"> 2/5</label><br>
  <label><input type="radio" name="movie" value="3_5"> 3/5</label><br>
  <label><input type="radio" name="movie" value="4_5"> 4/5</label><br>
  <label><input type="radio" name="movie" value="5_5"> 5/5</label>
</fieldset>

结果

在标记中引入sprite图片

对于这个解决方案,我们使用一个比上面例子更小的sprite。是由两个星星组成 (“on”和“off”)。

我们把img元素放在label标签内。我们假定当不支持css时,img不具有属性值,因此我们通过设置特定的尺寸(width和height属性)来隐藏它们。请注意,将两个属性值都设为0在有些用户代理下,会出现一个borken图片。


<form ...>
  <fieldset>
    <legend>Rating</legend>
    <label class="one" title="1 out of 5"><input name="LandOf" value="1" checked="checked" type="radio"> 1/5<img src="star-sprite.gif" alt="" height="0" width="0"></label>
    <label class="two" title="2 out of 5"><input name="LandOf" value="2" type="radio"> 2/5<img src="star-sprite.gif" alt="" height="0" width="0"></label>
    <label class="three" title="3 out of 5"><input name="LandOf" value="3" type="radio"> 3/5<img src="star-sprite.gif" alt="" height="0" width="0"></label>
    <label class="four" title="4 out of 5"><input name="LandOf" value="4" type="radio"> 4/5<img src="star-sprite.gif" alt="" height="0" width="0"></label>
    <label class="five" title="5 out of 5"><input name="LandOf" value="5" type="radio"> 5/5<img src="star-sprite.gif" alt="" height="0" width="0"></label>
  </fieldset>
</form>

请注意,使用上面的标记,(在大多数浏览器)下我们通过选择label可以选择field。

无障碍考虑

不幸的是 ,这种标记至少在两个屏幕阅读器下存在问题: JAWSNVDA (见这个bug的测试案例 )。这个问题与使用title属性和空字符串替代文本相关。

不使屏幕阅读器用户感到迷糊的解决方法是使用“星”(stars)作为替代文本( alt ),并在 鼠标悬停时使用JavaScript插入title。

更好的标记
<fieldset>
  <legend>Rating</legend>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="1_5"> 1/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="2_5"> 2/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="3_5"> 3/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="4_5"> 4/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="5_5"> 5/5</label>
</fieldset> 

结果

样式化

通过CSS设置图像尺寸

我们使用em ,使图片根据字体大小(font-size)放大或缩小。

标记

不变

CSS

img {
  width:2.8em;
  height:1.4em;
}

结果

正如您所看到的,点击一张图片时对应的单选按钮也被选中。这种行为无需使用添加隐式标记(implicit labeling)的脚本就可以实现(IE除外)。

从流中删除图像

为label设置position:relative并给图片设置position:absolute、top/left值就可以将input和文字隐藏在label内。

标记

不变

CSS

label {
  position:relative;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
}

结果

每个label显示一颗星

我们将label的尺寸设为一颗星的高度和宽度。

标记

不变

CSS

label {
  position:relative;
  height:1.4em;
  width:1.4em;
  overflow:hidden;
  display:block;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
}

结果

水平方式显式星星

我们移除br 并将label浮动。

标记

不变

CSS

br {
  display:none;
}
label {
  position:relative;
  height:1.4em;
  width:1.4em;
  overflow:hidden;
  display:block;
  float:left;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
}

结果

根据评级显式sprite图片
如果要设置三星级,我们对最后两个label应用一个相同的类。这个class用来改变图片在label中的位置。

标记

<fieldset>
<legend>Rating</legend>
<label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="1_5"> 1/5</label><br>
<label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="2_5"> 2/5</label><br>
<label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="3_5"> 3/5</label><br>
<label class="no_star"><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="4_5"> 4/5</label><br>
<label class="no_star"><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="5_5"> 5/5</label>
</fieldset>

CSS

br {
  display:none;
}
label {
  position:relative;
  height:1.4em;
  width:1.4em;
  overflow:hidden;
  float:left;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
}
.no_star img {
  left:-1.4em;
}

结果

不要单独依赖图片传递信息

当图片无法获取时,为星星设置替代文本是至关重要的。这是因为label和单选按钮的样式被设为重叠了。一个简单的解决方法是将input和文本移出屏幕(比如使用text-indent:-999em )和给label设置背景颜色。

标记

没有变化

CSS

br {
  display:none;
}
label {
  position:relative;
  height:1.4em;
  width:1.4em;
  overflow:hidden;
  float:left;
  background:teal;
  margin-right:1px;
  text-indent:-999em;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
}
.no_star {
  background:#ccc;
}
.no_star img {
  left:-1.4em;
}

注意 :

text-indent还修复了一个每当控件获得焦点时图片向上跳的问题。

右边距(margin-right)是为了确保背景颜色呈正方形而不是长方形(相邻的label使用相同的背景颜色时会出现这种情况)。

结果

画龙点睛

用伪类:hover创造出翻转效果,

隐藏fieldset的边框(border),

隐藏legend,

为光标(cursor)设置样式。

标记

不变

CSS

br {
  display:none;
}
label {
  position:relative;
  height:1.4em;
  width:1.4em;
  overflow:hidden;
  float:left;
  background:teal;
  margin-right:1px;
  text-indent:-999em;
}
input {
  position:absolute;
  left:-999em;
  top:.5em;
}
img {
  width:2.8em;
  height:1.4em;
  position:absolute;
  top:0;
  left:0;
  cursor: pointer;
}
.no_star {
  background:#ccc;
}
.no_star img {
  left:-1.4em;
}
label:hover {
  opacity:.5;
  filter:alpha(opacity=50);
}
fieldset {
  border:0;
}
legend {
  text-indent:-999em;
}

注:IE6会忽略label:hover以及在Opera下背景颜色会从图片中出血。在演示页,我使用了另一张含有四颗星的sprite而不是使用不透明度(opacity)。

结果

展示评级且用户不可交互

我们可以通过为对应的input添加disabled和checked属性,使评级“只读”。

标记

<fieldset>
  <legend>Rating</legend>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="1_5" disabled> 1/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="2_5" disabled> 2/5</label><br>
  <label><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="3_5" checked="checked"> 3/5</label><br>
  <label class="no_star"><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="4_5" disabled> 4/5</label><br>
  <label class="no_star"><img src="img/small-sprite.gif"  width="0" height="1" alt="stars"><input type="radio" name="movie" value="5_5" disabled> 5/5</label>
</fieldset> 

CSS

:hover规则已被删除

结果

更多思考

在这一点上,无需脚本支持就可完成投票,但对明眼用户来说对于他们的选择没有提示。因此,我们使用JavaScript来:

将用户的选择反馈给他们

为键盘用户提供视觉提示,便于他们在单选按钮之间导航。

与此同时,我们使用脚本增加title属性,当用户悬停在label或星星上时会出现工具提示(tooltips)。

由于不使用JavaScript会缺乏对选择的反馈,如果有脚本支持,我们可以为label和表单控件设置样式。为此,我们使用JavaScript在html元素上设置一个标志(flag),然后我们创建一个规则,基于包含那个钩子(hook)的后代选择器。如果不含有标志,这条规则就不应用,并且元素不被设置样式。

这是演示页 ,最终的作品。要了解这个解决方案根据不同设置是怎样表现的,您可能要使用你最喜欢的开发工具,来增加文字大小,更改图片路径,禁用JavaScript,关闭CSS等等…

总结

提出一个“可接受”的解决办法,需要识别用户的需求,用户代理的特殊性、用户代理的设置等 – 这意味着需要广泛的测试。

在这个过程中,用户的反馈非常重要,因为下面的最佳做法并不一定总是正确的。例如,如前面提到的,为label标签内的图片设置空值的title属性似乎是安全的,但事实证明,它至少在两个屏幕阅读器下存在问题(见测试案例)。

此外,辅助设备使用用户的建议是可以忽略一些验证错误信息——Firefox Accessibility 工具栏报告(根据http://bestpractices.cita.uiuc.edu/html/nav/form/ )。

本文的目的并不是解决一切问题。我的工作重点之一是能够不使用指点设备完成投票,改善解决方案在opera禁用图片时的外观和感觉并不是我的重点。

这个“征途”最有趣的部分是使解决方案对不同条件的用户无障碍,解决以下问题:

这个技术使用img元素,而不是背景图片,这使得星星可以:

所有这一切都没有牺牲性能,因为这个解决方案只使用了一张sprite图片:

之后发现

我最近发现亚马逊公司为其投票页面建立的系统。有趣的是他们根据是否支持脚本提供了不同的解决方案。如果支持脚本,他们使用一个图像<map>(有趣的方法),如果不支持脚本则使用单选按钮 。在这两种情况下,解决办法对键盘用户都是无障碍的 ,这有助于最大限度地使用一个功能,这是亚马逊平台的核心区别。

请注意,他们并不使用JavaScript将单选按钮用<map>代替,而是使用noscript元素包含单选按钮。

“开箱即用”的解决方案

Dreamweaver®
Spry Rating Widget
YUI
Star Rating Script for YUI
Star Rating script with YUI
JQuery
Half-Star Rating Plugin
jQuery Ajax Rater
Simple Star Rating System
5 star rating system in PHP, MySQL and jQuery
WordPress
GD Star Rating System for WordPress
GD Star Rating
Star Rating for Reviews
Flash
5 Star rating system component
Misc.
How a star rating should be
Starry widget 2

特别感谢

特别感谢Victor Tsaran和Todd Kloots提出的宝贵意见。