原文:Developing an Accessible Star Ratings Widget
作者:Thierry Koblentz
先睹为快,跳到演示页 。
许多电子商务网站、社交网络服务以及网上社区都包括评级或评估的功能。征集用户的意见已经成为一种商业模式,现在有许多网站致力于评价产品、服务、企业或其他。
最常见的显示投票的界面是“星级系统”,每个评论者给评审项目赋予一定数量的分值(通常以星表示)。很多网站都使用了这种模式,无论Amazon还是Yelp。
图A Amazon 和 Yelp 的星型评级
正如图A所示,两者的视觉界面类似,但使得这两种方案变得有趣的是它们使用的基本标记。一个使用的是<map> ,另一个是<img> 。
你也许会认为大多数评级系统所使用的标记是具有语义的,并在多种用户代理下都可以操作 – 也就是说,这种评级系统应当基于一组特定的HTML元素和属性集来构造,使用JS和CSS来控制行为和样式。如果是这样的话就好了,但事实远非如此。作者使用的标签可谓是千奇百怪:
<a>
,<img>
,<span>
,<li>
,<map>
,<div>
,<input>
,- and more…
微格式(Microformats)案例
在介绍几个基于图像的标记评级的技术之前,我认为值得提一下一个基本的和直接的方法( 来自微格式 ),使用字符 :
<abbr class="rating" title="3 stars">***</abbr>
优点
- 它是直接的并具有语义。
- 标记是最小的。
- 该方法不依赖于CSS 。
- 该方法不依赖于图像。
- 没有HTTP请求。
缺点
- 无法表示半数值(比如3.5星)
- 只表示星号(“星级”)
- 默认情况下,屏幕阅读器不能展开缩写(在这种情况下可能不是一个大问题)
注 :我选用“*”,而不是★(★),因为屏幕阅读器(至少JAWS和NVDA )似乎忽略HTML实体。
基于图片的评级标记
当涉及到显示图片,作者有很多选择。
每一个级别使用一张图片
使用一张图片:
<img src="4stars.png" alt="4 out of five">
- 一星
- 两星
- 三星
- 四星
- 五星
优点
- 每一个级别使用一张图片是直观的和具有语义。
- 该方法不依赖于CSS 。
- 最小的标记。
缺点
- 它会创建多个HTTP请求,因为有多张不同的图片。
- 除了性能问题,对于维护来说也是一场噩梦,需要耗费作者更多的资源(图片制作、图片投递到CDN、网站颜色改变时修改图片等)。
- Opera下,文本选择(Text selection )是不可能的 (至少在版本9.52下)因为替代文本会被忽略。
一组使用一张图片
来自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">
- 一星
- 二星
- 三星
- 四星
- 五星
优点
- 每次评级使用两个img元素减少了HTTP请求数量。
- 该方法不依赖于CSS 。
缺点
- 在Opera下,当图片被禁用时, 替代文本是不可选取的,以及(在小屏幕视图下),文字会带有边框(border)致使其清晰度降低。
请注意这个例子来自一个有争议的工作草案 。在我看来,这种方法是不能接受的,因为替代文字没有准确、简明地描述图像。此外,如果这种方法的依据是这些图片代表内容,那么为什么不给他们添加替代文本呢?
以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图
优点
- 这种方法只需要一个HTTP请求,因为它依赖一张sprite图片。
- 最小的“脚印”(foot print)。
缺点
- 在图片禁用时,无法揭示内容。
- 页面打印时,无法显示评分(打印样式表需要考虑这个问题)。
- 在Opera中,高对比度样式使所有的星星消失,高对比度模式优化(High Contrast Mode Optimization.)一文得到同样的结论 。
- 文本可以被选取,但它并不明显(通过高亮显示)。
标记中的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; }
优点
- 这种方法只需要一个HTTP请求。
- 这种技术是上面四种方法中唯一一个,在Firefox用户选择“隐藏图片”或“使图像不可见”(从开发者的工具栏)时, 仍能揭示内容。
- 当图像不可用时,一个红色的“X”仅出现在最高等级(即5/5),而不是在每一个等级上,因为它和其他解决方案一样依赖img元素。
缺点
- 这些图像的显示依赖于CSS 。
值得注意的是,它不像其他的图片替换技术,此方法允许:
- 根据文本大小(text-size)缩放图片。
- 图片可以被打印。
- 当整幅图片高亮显示时,替代文字可以很容易地选定(火狐)。
- 图像在高对比度设置/样式表下不会消失。
- 在Opera下,替代文字可被选择(当图片被禁用时)。
- 在Opera小屏幕视图下,替代文本没有边框(borderless)。
投票标记
机制
为了投票 ,我们需要一个低级别的投票机制,允许普通用户选择和提交。为此,我们可以使用表单:
标记
<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。
无障碍考虑
不幸的是 ,这种标记至少在两个屏幕阅读器下存在问题: JAWS和 NVDA (见这个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禁用图片时的外观和感觉并不是我的重点。
这个“征途”最有趣的部分是使解决方案对不同条件的用户无障碍,解决以下问题:
- 图像禁用,
- JavaScript禁用,
- CSS禁用,
- 以上的组合。
这个技术使用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提出的宝贵意见。