Skip to content

Commit 09a8d35

Browse files
committed
Use WCAG2 (Web Content Accessibility Guidelines V2.0) for readability
functions
1 parent e2b8def commit 09a8d35

File tree

3 files changed

+193
-70
lines changed

3 files changed

+193
-70
lines changed

README.md

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ Return a boolean indicating whether the color was successfully parsed. Note: if
133133
color2.isValid(); // false
134134
color2.toString(); // "#000000"
135135

136+
### getBrightness
137+
138+
Returns the perceived brightness of a color, from `0-255`, as defined by [Web Content Accessibility Guidelines (Version 1.0)](http://www.w3.org/TR/AERT#color-contrast).
139+
140+
var color1 = tinycolor("#fff");
141+
color1.getBrightness(); // 255
142+
143+
var color2 = tinycolor("#000");
144+
color2.getBrightness(); // 0
145+
136146
### isLight
137147

138148
Return a boolean indicating whether the color's perceived brightness is light.
@@ -153,6 +163,16 @@ Return a boolean indicating whether the color's perceived brightness is dark.
153163
var color2 = tinycolor("#000");
154164
color2.isDark(); // true
155165

166+
### getLuminance
167+
168+
Returns the perceived luminance of a color, from `0-1` as defined by [Web Content Accessibility Guidelines (Version 2.0).](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef)
169+
170+
var color1 = tinycolor("#fff");
171+
color1.getLuminance(); // 1
172+
173+
var color2 = tinycolor("#000");
174+
color2.getLuminance(); // 0
175+
156176
### getAlpha
157177

158178
Returns the alpha value of a color, from `0-1`.
@@ -166,16 +186,6 @@ Returns the alpha value of a color, from `0-1`.
166186
var color3 = tinycolor("transparent");
167187
color3.getAlpha(); // 0
168188

169-
### getBrightness
170-
171-
Returns the perceived brightness of a color, from `0-255`.
172-
173-
var color1 = tinycolor("#fff");
174-
color1.getBrightness(); // 255
175-
176-
var color2 = tinycolor("#000");
177-
color2.getBrightness(); // 0
178-
179189
### setAlpha
180190

181191
Sets the alpha value on a current color. Accepted range is in between `0-1`.
@@ -274,7 +284,7 @@ Print to a string, depending on the input format. You can also override this by
274284

275285
var color1 = tinycolor("red");
276286
color1.toString(); // "red"
277-
color1.toString("hsv"); // ""hsv(0, 100%, 100%)"
287+
color1.toString("hsv"); // "hsv(0, 100%, 100%)"
278288

279289
var color2 = tinycolor("rgb(255, 0, 0)");
280290
color2.toString(); // "rgb(255, 0, 0)"
@@ -396,29 +406,45 @@ Combination functions return an array of TinyColor objects unless otherwise note
396406

397407
### random
398408

399-
Returns a random color
409+
Returns a random color.
400410
```js
401411
var color = tinycolor.random();
402412
color.toRgb(); // "{r: 145, g: 40, b: 198, a: 1}"
403413
```
404414

405-
### readability
415+
### Readability
416+
TinyColor assesses readability based on the [Web Content Accessibility Guidelines (Version 2.0)](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef).
417+
418+
#### readability
419+
420+
`readability: function(TinyColor, TinyColor) -> Object`.
421+
Returns the contrast ratio between two colors.
406422

407-
`readable: function(TinyColor, TinyColor) -> Object`. Analyze 2 colors and returns an object with the following properties. `brightness` is difference in brightness between the two colors. `color`: difference in color/hue between the two colors.
423+
tinycolor.readability("#000", "#000"); // 1
424+
tinycolor.readability("#000", "#111"); // 1.1121078324840545
425+
tinycolor.readability("#000", "#fff"); // 21
408426

409-
tinycolor.readability("#000", "#111"); // {brightness: 17, color: 51}
410-
tinycolor.readability("#000", "#fff"); // {brightness: 255, color: 765}
427+
Use the values in your own calculations, or use one of the convenience functions below.
411428

412-
### isReadable
429+
#### isReadable
413430

414-
`isReadable: function(TinyColor, TinyColor) -> Boolean`. Ensure that foreground and background color combinations provide sufficient contrast.
431+
`isReadable: function(TinyColor, TinyColor, Object) -> Boolean`. Ensure that foreground and background color
432+
combinations meet WCAG guidelines. `Object` is optional, defaulting to defaults to `{level:"AA",size:"small"}`.
415433

416-
tinycolor.isReadable("#000", "#111"); // false
434+
tinycolor.isReadable("#000", "#111", {}); // false
435+
tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"small"}); //false
436+
tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"large"}), //true
417437

418-
### mostReadable
438+
#### mostReadable
419439

440+
`mostReadable: function(TinyColor, [TinyColor, Tinycolor ...], Object) -> Boolean`.
420441
Given a base color and a list of possible foreground or background colors for that base, returns the most readable color.
442+
If none of the colors in the list is readable, `mostReadable` will return the better of black or white if `includeFallbackColors:true`.
421443

422444
tinycolor.mostReadable("#000", ["#f00", "#0f0", "#00f"]).toHexString(); // "#00ff00"
445+
tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
446+
tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff"
447+
tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString() // "#2e0c3a",
448+
tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString() // "#000000",
423449

424-
See [index.html](https://github.com/bgrins/TinyColor/blob/master/index.html) in the project for a demo.
450+
See [index.html](https://github.com/bgrins/TinyColor/blob/master/index.html) in the project for a demo.

test/test.js

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,11 @@ test("getBrightness", function() {
396396
equal(tinycolor('#fff').getBrightness(), 255, 'returns 255 for #fff');
397397
});
398398

399+
test("getLuminance", function() {
400+
equal(tinycolor('#000').getLuminance(), 0, 'returns 0 for #000');
401+
equal(tinycolor('#fff').getLuminance(), 1, 'returns 1 for #fff');
402+
});
403+
399404
test("isDark returns true/false for dark/light colors", function() {
400405
equal(tinycolor('#000').isDark(), true, '#000 is dark');
401406
equal(tinycolor('#111').isDark(), true, '#111 is dark');
@@ -546,19 +551,64 @@ test("Color equality", function() {
546551
ok (tinycolor.equals("#ff8000", "rgb(100%, 50%, 0%)"), "Percentage bounds checking");
547552
});
548553
test("isReadable", function() {
549-
ok (tinycolor.isReadable("#000000", "#ffffff"), "white/black is readable");
550-
ok (!tinycolor.isReadable("#FF0088", "#8822AA"), "pink on pink is not readable");
554+
555+
// "#ff0088", "#8822aa" (values used in old WCAG1 tests)
556+
ok (tinycolor.isReadable("#000000", "#ffffff",{level:"AA",size:"small"}), "white/black is readable");
557+
ok (!tinycolor.isReadable("#ff0088", "#5c1a72",{}), "not readable - empty wcag2 object");
558+
ok (!tinycolor.isReadable("#ff0088", "#8822aa",{level:"AA",size:"small"}), "not readable - AA small");
559+
ok (!tinycolor.isReadable("#ff0088", "#8822aa",{level:"AA",size:"large"}), "not readable - AA large");
560+
ok (!tinycolor.isReadable("#ff0088", "#8822aa",{level:"AAA",size:"small"}), "not readable - AAA small");
561+
ok (!tinycolor.isReadable("#ff0088", "#8822aa",{level:"AAA",size:"large"}), "not readable - AAA large");
562+
563+
// values derived from and validated using the calculators at http://www.dasplankton.de/ContrastA/
564+
// and http://webaim.org/resources/contrastchecker/
565+
566+
// "#ff0088", "#5c1a72": contrast ratio 3.04
567+
ok (!tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"small"}), "not readable - AA small");
568+
ok (tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AA",size:"large"}), "readable - AA large");
569+
ok (!tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AAA",size:"small"}), "not readable - AAA small");
570+
ok (!tinycolor.isReadable("#ff0088", "#5c1a72",{level:"AAA",size:"large"}), "not readable - AAA large");
571+
572+
// "#ff0088", "#2e0c3a": contrast ratio 4.56
573+
ok (tinycolor.isReadable("#ff0088", "#2e0c3a",{level:"AA",size:"small"}), "readable - AA small");
574+
ok (tinycolor.isReadable("#ff0088", "#2e0c3a",{level:"AA",size:"large"}), "readable - AA large");
575+
ok (!tinycolor.isReadable("#ff0088", "#2e0c3a",{level:"AAA",size:"small"}), "not readable - AAA small");
576+
ok (tinycolor.isReadable("#ff0088", "#2e0c3a",{level:"AAA",size:"large"}), "readable - AAA large");
577+
578+
// "#db91b8", "#2e0c3a": contrast ratio 7.12
579+
ok (tinycolor.isReadable("#db91b8", "#2e0c3a",{level:"AA",size:"small"}), "readable - AA small");
580+
ok (tinycolor.isReadable("#db91b8", "#2e0c3a",{level:"AA",size:"large"}), "readable - AA large");
581+
ok (tinycolor.isReadable("#db91b8", "#2e0c3a",{level:"AAA",size:"small"}), "readable - AAA small");
582+
ok (tinycolor.isReadable("#db91b8", "#2e0c3a",{level:"AAA",size:"large"}), "readable - AAA large");
551583
});
584+
552585
test("readability", function() {
553-
// XXX: Need tests for readability
554-
deepEqual(tinycolor.readability("#000", "#111"), {brightness: 17, color: 51}, "Readability 1");
555-
deepEqual(tinycolor.readability("#000", "#fff"), {brightness: 255, color: 765}, "Readability 2");
586+
// check return values from readability function. See isReadable above for standards tests.
587+
equal(tinycolor.readability("#000", "#000"), 1, "Readability function test 0");
588+
deepEqual(tinycolor.readability("#000", "#111"), 1.1121078324840545, "Readability function test 1");
589+
deepEqual(tinycolor.readability("#000", "#fff"), 21, "Readability function test 2");
556590
});
557591
test("mostReadable", function () {
558-
equal (tinycolor.mostReadable("#000", ["#111", "#222"]).toHexString(), "#222222", "pick most readable color");
559-
equal (tinycolor.mostReadable("#f00", ["#d00", "#0d0"]).toHexString(), "#00dd00", "pick most readable color");
592+
equal (tinycolor.mostReadable("#000", ["#111", "#222",{wcag2:{}}]).toHexString(), "#222222", "readable color present");
593+
equal (tinycolor.mostReadable("#f00", ["#d00", "#0d0"],{wcag2:{}}).toHexString(), "#00dd00", "readable color present");
594+
equal (tinycolor.mostReadable("#fff", ["#fff", "#fff"],{wcag2:{}}).toHexString(), "#ffffff", "no different color in list");
595+
//includeFallbackColors
596+
equal (tinycolor.mostReadable("#fff", ["#fff", "#fff"],{includeFallbackColors:true}).toHexString(), "#000000", "no different color in list");
597+
equal (tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(), "#112255", "no readable color in list");
598+
equal (tinycolor.mostReadable("#123", ["#000", "#fff"],{includeFallbackColors:false}).toHexString(), "#ffffff", "verify assumption");
599+
equal (tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(), "#ffffff", "no readable color in list");
600+
601+
equal (tinycolor.mostReadable("#ff0088", ["#000", "#fff"],{includeFallbackColors:false}).toHexString(), "#000000", "verify assumption");
602+
equal (tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(), "#2e0c3a", "readable color present");
603+
equal (tinycolor.mostReadable("#ff0088", ["#2e0c3a"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(), "#000000", "no readable color in list");
604+
605+
equal (tinycolor.mostReadable("#371b2c", ["#000", "#fff"],{includeFallbackColors:false}).toHexString(), "#ffffff", "verify assumption");
606+
equal (tinycolor.mostReadable("#371b2c", ["#a9acb6"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(), "#a9acb6", "readable color present");
607+
equal (tinycolor.mostReadable("#371b2c", ["#a9acb6"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(), "#ffffff", "no readable color in list");
608+
560609
});
561610

611+
562612
test("Filters", function () {
563613

564614
equal (tinycolor("red").toFilter(), "progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ffff0000)");

tinycolor.js

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,23 @@ tinycolor.prototype = {
6969
return this._a;
7070
},
7171
getBrightness: function() {
72+
//http://www.w3.org/TR/AERT#color-contrast
7273
var rgb = this.toRgb();
7374
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
7475
},
76+
getLuminance: function() {
77+
//http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
78+
var rgb = this.toRgb();
79+
var RsRGB, GsRGB, BsRGB, R, G, B;
80+
RsRGB = rgb.r/255;
81+
GsRGB = rgb.g/255;
82+
BsRGB = rgb.b/255;
83+
84+
if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);}
85+
if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);}
86+
if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);}
87+
return (0.2126 * R) + (0.7152 * G) + (0.0722 * B);
88+
},
7589
setAlpha: function(value) {
7690
this._a = boundAlpha(value);
7791
this._roundA = mathRound(100*this._a) / 100;
@@ -689,68 +703,86 @@ tinycolor.mix = function(color1, color2, amount) {
689703

690704
// Readability Functions
691705
// ---------------------
692-
// <http://www.w3.org/TR/AERT#color-contrast>
706+
// <http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef (WCAG Version 2)
693707

694-
// `readability`
695-
// Analyze the 2 colors and returns an object with the following properties:
696-
// `brightness`: difference in brightness between the two colors
697-
// `color`: difference in color/hue between the two colors
708+
// `contrast`
709+
// Analyze the 2 colors and returns the color contrast defined by (WCAG Version 2)
698710
tinycolor.readability = function(color1, color2) {
699711
var c1 = tinycolor(color1);
700712
var c2 = tinycolor(color2);
701-
var rgb1 = c1.toRgb();
702-
var rgb2 = c2.toRgb();
703-
var brightnessA = c1.getBrightness();
704-
var brightnessB = c2.getBrightness();
705-
var colorDiff = (
706-
Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
707-
Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
708-
Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
709-
);
710-
711-
return {
712-
brightness: Math.abs(brightnessA - brightnessB),
713-
color: colorDiff
714-
};
713+
return (Math.max(c1.getLuminance(),c2.getLuminance())+0.05) / (Math.min(c1.getLuminance(),c2.getLuminance())+0.05);
715714
};
716715

717-
// `readable`
718-
// http://www.w3.org/TR/AERT#color-contrast
719-
// Ensure that foreground and background color combinations provide sufficient contrast.
716+
// `isReadable`
717+
// Ensure that foreground and background color combinations meet WCAG2 guidelines.
718+
// The third argument is an optional Object.
719+
// the 'level' property states 'AA' or 'AAA' - if missing or invalid, it defaults to 'AA';
720+
// the 'size' property states 'large' or 'small' - if missing or invalid, it defaults to 'small'.
721+
// If the entire object is absent, isReadable defaults to {level:"AA",size:"small"}.
722+
720723
// *Example*
721724
// tinycolor.isReadable("#000", "#111") => false
722-
tinycolor.isReadable = function(color1, color2) {
725+
// tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false
726+
727+
tinycolor.isReadable = function(color1, color2, wcag2) {
723728
var readability = tinycolor.readability(color1, color2);
724-
return readability.brightness > 125 && readability.color > 500;
729+
var wcag2Parms, out;
730+
731+
out = false;
732+
733+
wcag2Parms = validateWCAG2Parms(wcag2);
734+
switch (wcag2Parms.level + wcag2Parms.size) {
735+
case "AAsmall":
736+
case "AAAlarge":
737+
out = readability >= 4.5;
738+
break;
739+
case "AAlarge":
740+
out = readability >= 3;
741+
break;
742+
case "AAAsmall":
743+
out = readability >= 7;
744+
break;
745+
}
746+
return out;
747+
725748
};
726749

727750
// `mostReadable`
728751
// Given a base color and a list of possible foreground or background
729752
// colors for that base, returns the most readable color.
753+
// Optionally returns Black or White if the most readable color is unreadable.
730754
// *Example*
731-
// tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
732-
tinycolor.mostReadable = function(baseColor, colorList) {
733-
var bestColor = null;
734-
var bestScore = 0;
735-
var bestIsReadable = false;
736-
for (var i=0; i < colorList.length; i++) {
755+
// tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255"
756+
// tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff"
757+
// tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3"
758+
// tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff"
737759

738-
// We normalize both around the "acceptable" breaking point,
739-
// but rank brightness constrast higher than hue.
740760

741-
var readability = tinycolor.readability(baseColor, colorList[i]);
742-
var readable = readability.brightness > 125 && readability.color > 500;
743-
var score = 3 * (readability.brightness / 125) + (readability.color / 500);
744-
745-
if ((readable && ! bestIsReadable) ||
746-
(readable && bestIsReadable && score > bestScore) ||
747-
((! readable) && (! bestIsReadable) && score > bestScore)) {
748-
bestIsReadable = readable;
749-
bestScore = score;
761+
tinycolor.mostReadable = function(baseColor, colorList, args) {
762+
var bestColor = null;
763+
var bestScore = 0;
764+
var readability;
765+
var includeFallbackColors, level, size ;
766+
args = args || {};
767+
includeFallbackColors = args.includeFallbackColors ;
768+
level = args.level;
769+
size = args.size;
770+
771+
for (var i= 0; i < colorList.length ; i++) {
772+
readability = tinycolor.readability(baseColor, colorList[i]);
773+
if (readability > bestScore) {
774+
bestScore = readability;
750775
bestColor = tinycolor(colorList[i]);
751776
}
752777
}
753-
return bestColor;
778+
779+
if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) {
780+
return bestColor;
781+
}
782+
else {
783+
args.includeFallbackColors=false;
784+
return tinycolor.mostReadable(baseColor,["#fff", "#000"],args);
785+
}
754786
};
755787

756788

@@ -1100,6 +1132,21 @@ function stringInputToObject(color) {
11001132
return false;
11011133
}
11021134

1135+
function validateWCAG2Parms(parms) {
1136+
// return valid WCAG2 parms for isReadable.
1137+
// If input parms are invalid, return {"level":"AA", "size":"small"}
1138+
var level, size;
1139+
parms = parms || {"level":"AA", "size":"small"};
1140+
level = (parms.level || "AA").toUpperCase();
1141+
size = (parms.size || "small").toLowerCase();
1142+
if (level !== "AA" && level !== "AAA") {
1143+
level = "AA";
1144+
}
1145+
if (size !== "small" && size !== "large") {
1146+
size = "small";
1147+
}
1148+
return {"level":level, "size":size};
1149+
}
11031150
// Node: Export function
11041151
if (typeof module !== "undefined" && module.exports) {
11051152
module.exports = tinycolor;

0 commit comments

Comments
 (0)