Select font characteristics and background colors to provide enough contrast for readability

Use tools to evaluate font size, font stroke width, background color, font color, and nearby colors and adjust the properties of those elements to achieve good visual contrast and readability.

This method contains helpful information, but is not required (informative).

Basics

Status

Platform

Web

Programming Language

How It Solves User Need

All sighted users need adequate luminance contrast (lightness/darkness difference) between background and text colors in order to read the text easily.

Related Guidelines

Detailed Description

Understanding Contrast Perception

As used on this page, Visual Contrast means the perceived difference between two colors displayed on a computer monitor, such as the color of the text against the color of the background. Our perception of contrast is a function of the Human Visual System (HVS) which includes the eye, optic nerve, and several areas of the brain that are either dedicated to processing vision, or that share vision processing with other senses such as hearing.

The eye consists of an optical portion including a lens, and a light sensitive portion known as the retina. Muscles in the eye adjust the shape of the lens to adjust the focus of incoming light, so that an image of the world around us is focused onto the retina. Light sensitive cells on the retina then convert light into electrical impulses that are sent to the brain's vision processing center, called the visual cortex.

This complex system is subject to many forms of impairment. And even "normal" vision changes substantially over a person's lifetime. For instance contrast sensitivity is very poor at birth, and it takes 20 years of development for normal vision to reach peak contrast sensitivity. Many people over the age of 45 develop the need for reading glasses as the natural lens in their eye grows larger and stiffer to where the muscles can no longer pull it into focus. And while people often think of acuity as a defining factor (i.e. 20/20, the ability to focus), vision is often more impacted by contrast sensitivity related impairments.

Our perception of contrast and ultimately the readability of text is affected by many interdependent factors, including:

  • The user's personal vision quality in terms of sharpness, glare, and contrast sensitivity
  • Font weight or stroke width, and font size
  • The lightness or darkness difference between the background and the text
  • The overall lightness or darkness of the screen and the light in the room as this affects the eye's light or dark adaptation to the environment
  • The padding and spacing around the text which affects the eye's local adaptation to the text area
  • The hue or color difference between the background and text, not including the lightness difference mentioned above

All of these interdependent factors affect the perception of contrast and ultimately readability. As we are concerned with readability on computer screens, we can made some assumptions about a range of brightness, and a typical viewing environment. This provides a baseline standard for all users for best readability of content, or a minimum level of legibility for non-content. Then, we can set how different impairments require different adjustments or user customization to accommodate the needs of each individual user.

For instance, a user with poor "acuity", that is, poor sharpness where they cannot focus on small text, will need to be able to zoom the text larger. A user with poor contrast sensitivity may have good sharpness, but unable to perceive colors that are too similar in lightness. Yet another user with a glare problem may need lower contrast, and/or a reduction in blue, to reduce glare or halos interfering with readability. Those with a color vision problem (sometimes called 'color blind') may have difficulty detecting contrast or lightness for certain colors, such as deep red.

Predicted Contrast

Predicted contrast is reported as a percentage using the methods in this guideline, based on the CSS color values in sRGB colorspace, and with device default antialiasing1. The Tests section has a lookup table for specific font and contrast combinations, but as a general guide:

  • 25% is the point of invisibility (i.e., no perceptible contrast) for many people with contrast related impairments. Designers should assume that contrasts lower than this may be invisible to some users.
  • 40% for Large, Bold Headlines where the major stroke width is at least 8px (6pt), or Non-text elements that are at least a solid 8x8px square such as buttons.
  • 60% For Bold Text no less than 16px (12pt) or non-text elements no less than 3px in the thinnest dimension.
  • 80% Normal Weight Text no less than 16px (12pt) or non-text with a minimum stroke of 2px.
  • 100% 300 Weight Text no less than 16px (12pt) or non-text with a minimum stroke of 1px.

Note: 1These values require that antialiasing be at the device or user agent default, such that added antialiasing such as "Webkit-Font-Smoothing: Antialias;" is not enabled.

Related Methods

Use default fonts and colors.

Alternate Meanings

It's useful to point out that the term “Visual Contrast” can also be used to describe the differences of other visual perceptions: Contrast of size difference, contrast of position, contrast of speed or motion, etc. but these are not covered in this guideline at this time.

Code Samples

Example Code for a Test Tool


////////////////////////////////////////////////////////////////////////////////
/////	Functions to parse color values and determine SAPC contrast
/////	REQUIREMENTS: ECMAScript 6 - ECMAScript 2015
/////	SAPC tool version 0.97 by Andrew Somers
/////	https://www.myndex.com/WEB/Perception
/////	Color value input parsing based substantially on rgbcolor.js by
/////	Stoyan Stefanov <sstoo@gmail.com>
/////	His site: http://www.phpied.com/rgb-color-parser-in-javascript/
/////	MIT license
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/////
/////	*****  SAPC BLOCK  *****
/////
/////	For Evaluations, this is referred to as: SAPC-7
/////	sRGB Advanced Perceptual Contrast v0.97 beta JAVASCRIPT
/////	Copyright © 2019 by Andrew Somers
/////	Licensed to the W3C Per Collaborator Agreement
/////	SIMPLE VERSION — This Version Is Stripped Of Extensions:
/////		* No Color Vision Module
/////		* No Spatial Frequency Module
/////		* No Light Adaptation Module
/////		* No Dynamics Module
/////		* No Alpha Module
/////
////////////////////////////////////////////////////////////////////////////////


///// CONSTANTS USED IN THIS VERSION ///////////////////////////////////////////

	const sRGBtrc = 2.218;	// Gamma for sRGB linearization. 2.223 could be used instead
							// 2.218 sets unity with the piecewise sRGB at #777

	const Rco = 0.2126;		// sRGB Red Coefficient
	const Gco = 0.7156;		// sRGB Green Coefficient
	const Bco = 0.0722;		// sRGB Blue Coefficient

	const scaleBoW = 161.8;	// Scaling for dark text on light (phi * 100)
	const scaleWoB = 161.8;	// Scaling for light text on dark — same as BoW, but
							// this is separate for possible future use.

	const normBGExp = 0.38;		// Constants for Power Curve Exponents.
	const normTXTExp = 0.43;	// One pair for normal text,and one for REVERSE
	const revBGExp = 0.5;		// FUTURE: These will eventually be dynamic
	const revTXTExp = 0.43;		// as a function of light adaptation and context

	const blkThrs = 0.02;	// Level that triggers the soft black clamp
	const blkClmp = 1.75;	// Exponent for the soft black clamp curve

///// Ultra Simple Basic Bare Bones SAPC Function //////////////////////////////

	// This REQUIRES linearized R,G,B values of 0.0-1.0

function SAPCbasic(Rbg,Gbg,Bbg,Rtxt,Gtxt,Btxt) {

	var	SAPC = 0.0;

	// Find Y by applying coefficients and sum.
	// This REQUIRES linearized R,G,B 0.0-1.0
	
	var	Ybg = Rbg*Rco + Gbg*Gco + Bbg*Bco;
	var	Ytxt = Rtxt*Rco + Gtxt*Gco + Btxt*Bco;

		/////	INSERT COLOR MODULE HERE	/////

	// Now, determine polarity, soft clamp black, and calculate contrast
	// Finally scale for easy to remember percentages
	// Note that reverse (white text on black) intentionally
	// returns a negative number

	if ( Ybg > Ytxt ) {	///// For normal polarity, black text on white

			// soft clamp darkest color if near black.
		Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + Math.abs(Ytxt - blkThrs) ** blkClmp;
		SAPC = ( Ybg ** normBGExp - Ytxt ** normTXTExp ) * scaleBoW;
		
		return (SAPC < 15 ) ? "0%" : SAPC.toPrecision(3) + "%";
		
	} else {			///// For reverse polarity, white text on black

		Ybg = (Ybg > blkThrs) ? Ybg : Ybg + Math.abs(Ybg - blkThrs) ** blkClmp;
		SAPC = ( Ybg ** revBGExp - Ytxt ** revTXTExp ) * scaleWoB;

		return (SAPC > -15 ) ? "0%" : SAPC.toPrecision(3) + "%";
	}

	// If SAPC's more than 15%, return that value, otherwise clamp to zero
	// this is to remove noise and unusual behavior if the user inputs
	// colors too close to each other.
	// This will be more important with future modules. Nevertheless
	// In order to simplify code, SAPC will not report accurate contrasts
	// of less than approximately 15%, so those are clamped. 
	// 25% is the "point of invisibility" for many people.

}

//////////////////////////////////////////////////////////////
///// END OF SAPC BLOCK             //////////////////////////
//////////////////////////////////////////////////////////////



//////////////////////////////////////////////////////////////
///// sRGB INPUT FORM BLOCK         //////////////////////////
//////////////////////////////////////////////////////////////


function RGBColor(color_string) {
	
	this.ok = false;

	// strip any leading #
	if (color_string.charAt(0) == '#') {	// remove # if any
		color_string = color_string.substr(1,6);
	}
	color_string = color_string.replace(/ /g,'');	// strip spaces
	color_string = color_string.toLowerCase();		// set lowercase

	// before getting into regexps, try simple matches
	// and overwrite the input

	var simple_colors = {
		aliceblue: 'f0f8ff',
		antiquewhite: 'faebd7',
		aqua: '00ffff',
		aquamarine: '7fffd4',
		azure: 'f0ffff',
		beige: 'f5f5dc',
		bisque: 'ffe4c4',
		black: '000000',
		blanchedalmond: 'ffebcd',
		blue: '0000ff',
		blueviolet: '8a2be2',
		brown: 'a52a2a',
		burlywood: 'deb887',
		cadetblue: '5f9ea0',
		chartreuse: '7fff00',
		chocolate: 'd2691e',
		coral: 'ff7f50',
		cornflowerblue: '6495ed',
		cornsilk: 'fff8dc',
		crimson: 'dc143c',
		cyan: '00ffff',
		darkblue: '00008b',
		darkcyan: '008b8b',
		darkgoldenrod: 'b8860b',
		darkgray: 'a9a9a9',
		darkgreen: '006400',
		darkkhaki: 'bdb76b',
		darkmagenta: '8b008b',
		darkolivegreen: '556b2f',
		darkorange: 'ff8c00',
		darkorchid: '9932cc',
		darkred: '8b0000',
		darksalmon: 'e9967a',
		darkseagreen: '8fbc8f',
		darkslateblue: '483d8b',
		darkslategray: '2f4f4f',
		darkturquoise: '00ced1',
		darkviolet: '9400d3',
		deeppink: 'ff1493',
		deepskyblue: '00bfff',
		dimgray: '696969',
		dodgerblue: '1e90ff',
		feldspar: 'd19275',
		firebrick: 'b22222',
		floralwhite: 'fffaf0',
		forestgreen: '228b22',
		fuchsia: 'ff00ff',
		gainsboro: 'dcdcdc',
		ghostwhite: 'f8f8ff',
		gold: 'ffd700',
		goldenrod: 'daa520',
		gray: '808080',
		green: '008000',
		greenyellow: 'adff2f',
		honeydew: 'f0fff0',
		hotpink: 'ff69b4',
		indianred : 'cd5c5c',
		indigo : '4b0082',
		ivory: 'fffff0',
		khaki: 'f0e68c',
		lavender: 'e6e6fa',
		lavenderblush: 'fff0f5',
		lawngreen: '7cfc00',
		lemonchiffon: 'fffacd',
		lightblue: 'add8e6',
		lightcoral: 'f08080',
		lightcyan: 'e0ffff',
		lightgoldenrodyellow: 'fafad2',
		lightgrey: 'd3d3d3',
		lightgreen: '90ee90',
		lightpink: 'ffb6c1',
		lightsalmon: 'ffa07a',
		lightseagreen: '20b2aa',
		lightskyblue: '87cefa',
		lightslateblue: '8470ff',
		lightslategray: '778899',
		lightsteelblue: 'b0c4de',
		lightyellow: 'ffffe0',
		lime: '00ff00',
		limegreen: '32cd32',
		linen: 'faf0e6',
		magenta: 'ff00ff',
		maroon: '800000',
		mediumaquamarine: '66cdaa',
		mediumblue: '0000cd',
		mediumorchid: 'ba55d3',
		mediumpurple: '9370d8',
		mediumseagreen: '3cb371',
		mediumslateblue: '7b68ee',
		mediumspringgreen: '00fa9a',
		mediumturquoise: '48d1cc',
		mediumvioletred: 'c71585',
		midnightblue: '191970',
		mintcream: 'f5fffa',
		mistyrose: 'ffe4e1',
		moccasin: 'ffe4b5',
		navajowhite: 'ffdead',
		navy: '000080',
		oldlace: 'fdf5e6',
		olive: '808000',
		olivedrab: '6b8e23',
		orange: 'ffa500',
		orangered: 'ff4500',
		orchid: 'da70d6',
		palegoldenrod: 'eee8aa',
		palegreen: '98fb98',
		paleturquoise: 'afeeee',
		palevioletred: 'd87093',
		papayawhip: 'ffefd5',
		peachpuff: 'ffdab9',
		peru: 'cd853f',
		pink: 'ffc0cb',
		plum: 'dda0dd',
		powderblue: 'b0e0e6',
		purple: '800080',
		red: 'ff0000',
		rosybrown: 'bc8f8f',
		royalblue: '4169e1',
		saddlebrown: '8b4513',
		salmon: 'fa8072',
		sandybrown: 'f4a460',
		seagreen: '2e8b57',
		seashell: 'fff5ee',
		sienna: 'a0522d',
		silver: 'c0c0c0',
		skyblue: '87ceeb',
		slateblue: '6a5acd',
		slategray: '708090',
		snow: 'fffafa',
		springgreen: '00ff7f',
		steelblue: '4682b4',
		tan: 'd2b48c',
		teal: '008080',
		thistle: 'd8bfd8',
		tomato: 'ff6347',
		turquoise: '40e0d0',
		violet: 'ee82ee',
		violetred: 'd02090',
		wheat: 'f5deb3',
		white: 'ffffff',
		whitesmoke: 'f5f5f5',
		yellow: 'ffff00',
		yellowgreen: '9acd32'
	};

	for (var key in simple_colors) {
		if (color_string == key) {
			color_string = simple_colors[key];
		}
	}
	// end of simple type-in colors



	// array of color definition objects
	var color_defs = [
		{
			re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
			example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
			process: function (bits){
				return [
					parseInt(bits[1]),
					parseInt(bits[2]),
					parseInt(bits[3])
				];
			}
		},
		{
			re: /^(\w{2})(\w{2})(\w{2})$/,
			example: ['#00ff00', '336699'],
			process: function (bits){
				return [
					parseInt(bits[1], 16),
					parseInt(bits[2], 16),
					parseInt(bits[3], 16)
				];
			}
		},
		{
			re: /^(\w{1})(\w{1})(\w{1})$/,
			example: ['#fb0', 'f0f'],
			process: function (bits){
				return [
					parseInt(bits[1] + bits[1], 16),
					parseInt(bits[2] + bits[2], 16),
					parseInt(bits[3] + bits[3], 16)
				];
			}
		}
	];



	// search through the definitions to find a match
	for (var i = 0; i < color_defs.length; i++) {
		var re = color_defs[i].re;
		var processor = color_defs[i].process;
		var bits = re.exec(color_string);
		if (bits) {
			channels = processor(bits);
			this.r = channels[0];
			this.g = channels[1];
			this.b = channels[2];
			this.ok = true;
		}
	}


	// validate & cleanup values
	this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
	this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
	this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);



	// some getters

	// returns rgb() value string
	this.toRGB = function () {
		return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
	}

	// returns rgb() value string
	this.toRGB2 = 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';


	// returns hex string
	this.toHex = function () {
		var r = this.r.toString(16);
		var g = this.g.toString(16);
		var b = this.b.toString(16);
		if (r.length == 1) r = '0' + r;
		if (g.length == 1) g = '0' + g;
		if (b.length == 1) b = '0' + b;
		return '#' + r + g + b;
	}

	// returns decimal array for R, G, and B
	this.toDec = function () {
		return [this.r/255.0,this.g/255.0,this.b/255.0];
	}

	// these return linearized R, G, or B

	this.Rlin = function () {
		return Math.pow(this.r/255.0, sRGBtrc);
	}
	this.Glin = function () {
		return Math.pow(this.g/255.0, sRGBtrc);
	}
	this.Blin = function () {
		return Math.pow(this.b/255.0, sRGBtrc);
	}


	this.toY = function () {
		return 	Math.pow(this.r/255.0, sRGBtrc) * Rco + Math.pow(this.g/255.0, sRGBtrc) * Gco + Math.pow(this.b/255.0, sRGBtrc) * Bco;
	}
}

//////////////////////////////////////////////////////////////
///// END sRGB INPUT FORM BLOCK 	//////////////////////
//////////////////////////////////////////////////////////////

HTML and Page Level Scripts



<?xml version="1.1" encoding="utf-8"?>
<!DOCTYPE html>
<head>
<title>SAPC Contrast Demo</title>

<script src="SAPCsRGB.js">
// Place the above blocks in file SAPCsRGB.js
</script>

<script>
		function getBGColor(s) {
			var BGcolor = new RGBColor(s);
			if (BGcolor.ok) {
			
				document.getElementById('BGresult').style.backgroundColor
					= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
				document.getElementById('sansSamples').style.backgroundColor
					= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
				document.getElementById('serifSamples').style.backgroundColor
					= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
					
				document.getElementById('BGresult-text').innerHTML =
					'<code><b>HEX:</b> ' + BGcolor.toHex()
					  + ' <b>sRGB:</b> ' + BGcolor.toRGB()
					  + '<br><b>Decimal:</b> rgb( ' + BGcolor.toDec()[0].toPrecision(2)
					  + ', ' + BGcolor.toDec()[1].toPrecision(2)
					  + ', ' + BGcolor.toDec()[2].toPrecision(2)
					  + ' )<br><b>Lin:</b> R ' + BGcolor.Rlin().toPrecision(3)
					  + '  G ' + BGcolor.Glin().toPrecision(3)
					  + '  B ' + BGcolor.Blin().toPrecision(3)
					  + '<br><b>Luminance:</b> Y ' + BGcolor.toY().toPrecision(3) + '</code>'
					  ;
			} else {
				document.getElementById('BGresult-text').innerHTML = 'Invalid Color';
				document.getElementById('BGresult').style.backgroundColor
					= 'rgb(255, 255, 255)';
			}
		}

		function getTextColor(s) {
			var textColor = new RGBColor(s);
			if (textColor.ok) {
			
				document.getElementById('textResult').style.backgroundColor
					= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';
				document.getElementById('sansSamples').style.color
					= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';
				document.getElementById('serifSamples').style.color
					= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';	
					
				document.getElementById('textResult-text').innerHTML =
					'<code><b>HEX:</b> ' + textColor.toHex()
					  + ' <b>sRGB:</b> ' + textColor.toRGB()
					  + '<br><b>Decimal:</b> rgb( ' + textColor.toDec()[0].toPrecision(2)
					  + ', ' + textColor.toDec()[1].toPrecision(2)
					  + ', ' + textColor.toDec()[2].toPrecision(2)
					  + ' )<br><b>Lin:</b> R ' + textColor.Rlin().toPrecision(3)
					  + '  G ' + textColor.Glin().toPrecision(3)
					  + '  B ' + textColor.Blin().toPrecision(3)
					  + '<br><b>Luminance:</b> Y ' + textColor.toY().toPrecision(3) + '</code>'
					  ;
			} else {
				document.getElementById('textResult-text').innerHTML = 'Invalid Color';
				document.getElementById('textResult').style.backgroundColor = 'rgb(255, 255, 255)';
			}
		}
		
	</script>
</head>
<body onLoad="document.getElementById('inputBG').focus(); document.getElementById('inputText').focus(); document.getElementById('inputBG').focus(); testContrast(); ">

	<div id="demoArea"> 
	
		<div id="contrast">        
			<div id="contrastLabel">SAPC Contrast</div>
			<div id="contrastResult">NaN</div>
        </div>
       
        <h1 style="text-align: left; ">SAPC Visual Contrast Demo</h1>

        <div class="">
			This is the Basic SAPC contrast method using JavaScript. 
			<br>Just type in color values as #hex, rgb(), or HTML name 
			<br>then navigate away from the input field by either
			<br>pressing TAB or clicking elsewhere on the page.
        </div>
    </div>           


	<div id="inputArea">        
		<div style="position: relative; float: left;">
			<h3>ENTER BACKGROUND COLOR
			</h3>
			<form id="colorForm" name="colorForm" onsubmit="getBGColor(this.elements[0].value); getTextColor(this.elements[1].value); return false;">
				<div class="input">
					<input
						id="inputBG"
						type="text"
						value="#BA9"
						onblur="getBGColor(this.value); testContrast();">
				</div>
		
			<div id="BGresult-wrap"><div id="BGresult"></div></div>
			<div class="codeBlock" id="BGresult-text"></div>
	   </div>
	
        
		<div style="position: relative; float: right;">
			<h3>ENTER TEXT COLOR
			</h3>
				<div class="input">
					<input
						id="inputText"
						type="text"
						value="#319"
						onblur="getTextColor(this.value); testContrast();">
				</div>
			</form>
			<div id="textResult-wrap"><div id="textResult"></div></div>
			<code id="textResult-text"></code>
	
		</div>
	</div>

<div style="font-size: 12px;">The page and code is Copyright © 2020 by Andrew Somers.
<br>Licensed to the W3.org per their cooperative agreement. 
<br>Otherwise under the MIT license. 
<br>Repository: <a href="https://github.com/Myndex/SAPC/tree/master/JS">https://github.com/Myndex/SAPC/tree/master/JS</a>
<br>Color value input parsing based substantially on rgbcolor.js by
<br>Stoyan Stefanov <sstoo@gmail.com> used per MIT license.
<br>
This project is part of the <a href="https://www.myndex.com/WEB/Perception">Myndex Web Perception Experiment.
</a>
<br><br>
    <span onclick="testContrast()" style="cursor: pointer; text-decoration: underline">Manual Refresh</span>

<script>
	function testContrast() {
		var BG = new RGBColor(document.getElementById('inputBG').value);
		var txt = new RGBColor(document.getElementById('inputText').value);
		
		document.getElementById('contrastResult').innerHTML =  SAPCbasic(BG.Rlin(),BG.Glin(),BG.Blin(),txt.Rlin(),txt.Glin(),txt.Blin());
	}
</script>		
</body>
</html>


Tests

Procedure

  1. Using source code inspection or an “eye dropper” type tool, obtain representative sRGB values for the foreground text and the background color.
    • If an “eye dropper” type tool is used it must report values relative to the sRGB colorspace.
    • The eye dropper sample should be a pixel in the middle of a major stroke of the font at the smallest size to be tested, with the content size set to default with no user scaling.
  2. Using an automated tool like Advanced Perceptual Contrast Algorithm (APCA) Visual Contrast Demo to calculate the predicted contrast between foreground text and background color.
    • Important: do not swap the background or text colors in the tool entry fields.
    • The APCA tool predicts contrast based in part on polarity, so it is important that the text color CSS value be entered into the text color field, and likewise for the background.
  3. Compare this calculated value against the lookup table "Accessible Contrast by Font Size and Weight".
  4. Check that the absolute value of the predicted contrast percentage meets or exceeds the required value for the font weight and size.

Accessible Contrast by Font Size and Weight

Directions:

FontFont Weight
Size100200300400
(Normal)
500600700
(Bold)
800900
12px100%94%87%80%80%80%
14px90%84%77%70%70%70%
16px110%80%77%72%60%60%60%
18px100%78%74%70%59%59%59%
20px120%94%76%72%67%58%58%58%
22px110%87%75%70%65%57%57%57%
24px100%80%74%66%60%56%56%56%
26px96%78%72%65%59%55%55%55%
28px92%76%70%64%58%54%54%52%
30px88%74%68%62%57%53%52%50%
32px84%72%66%60%56%52%50%48%
36px120%80%70%64%58%54%50%48%46%
40px114%77%68%62%57%52%48%46%44%
44px108%74%66%60%55%50%46%44%42%
48px100%70%65%58%53%48%44%42%40%
56px95%67%64%56%51%46%42%40%40%
64px90%65%62%54%49%44%40%40%40%
72px85%63%60%52%47%42%40%40%40%
96px80%60%55%50%45%40%40%40%40%

Expected Results

Resources

APCA Math

APCA is the Advanced Perceptual Contrast Algorithm. The math assumes the use of the web standard sRGB colorspace.

The simplified steps are:

  • Convert the sRGB background and text colors to luminance Ybg and Ytxt
    • Convert from 8 bit integer to decimal 0.0-1.0
    • Linearize (remove gamma)
    • Apply sRGB coefficients and sum to Y
  • Determine which is brighter for contrast polarity
  • Apply pre-process modules (clamp, FUTURE: color, spatial, adaptation)
  • Apply power curve exponents for perceptual lightness
  • Find difference and scale to output percentage

Basic APCA Math Pseudocode

In the sRGB colorspace, using CSS color values as integers, with a background color sRGBbg and a text color sRGBtxt convert each channel to decimal 0.0-1.0 by dividing by 255, then linearize the gamma encoded RGB channels by applying a simple exponent.

	Rlinbg = (sRbg/255.0) ^ 2.218
	Glinbg = (sGbg/255.0) ^ 2.218
	Blinbg = (sBbg/255.0) ^ 2.218
	Rlintxt = (sRtxt/255.0) ^ 2.218
	Glintxt = (sGtxt/255.0) ^ 2.218
	Blintxt = (sBtxt/255.0) ^ 2.218

Then find the relative luminance (Y) of each color by applying the sRGB/Rec709 spectral coefficients and summing together.

	Ybg = 0.2126 * Rlinbg + 0.7156 * Glinbg + 0.0722 * Blinbg
	Ytxt = 0.2126 * Rlintxt + 0.7156 * Glintxt + 0.0722 * Blintxt 

Predicted Contrast

The Predicted Visual Contrast (APCA) between a foreground color and a background color is expressed as a percentage and is calculated by:

//  Define Constants for Basic APCA Version:

normBGExp = 0.38;	// Constants for Power Curve Exponents.
normTXTExp = 0.43;	// One pair for normal text, and one for REVERSE
revBGExp = 0.5;		// FUTURE: These will eventually be dynamic
revTXTExp = 0.43;	// as a function of light adaptation and context

blkThrs = 0.02;		// Level that triggers the soft black clamp
blkClmp = 1.75;		// Exponent for the soft black clamp curve

//  Calculate Predicted Contrast and return a string for the result

if Ybg > Ytxt then {
    Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + abs(Ytxt - blkThrs) ^ blkClmp;
    APCA = ( Ybg ^ normBGExp - Ytxt ^ normTXTExp ) * 161.8;
    return (APCA < 15 ) ? "0%" : str(APCA) + "%";
  } else {
    Ybgg = (Ybg > blkThrs) ? Ybg : Ybg + abs(Ybg - blkThrs) ^ blkClmp;
    APCA = ( Ybg ^ revBGExp - Ytxt ^ revTXTExp ) * 161.8;
    return (APCA > -15 ) ? "0%" : str(APCA) + "%";
}  
    
Notes:

Predicted contrast less than 15% is clamped to zero to simplify the math.

We will use the simple exponent, and not the piecewise sRGB transfer curve, as we are emulating display gamma and not performing image processing.

The “^” character is the exponentiation operator.


Glossary


Bibliography and References

Changelog