{"id":"50f544d08738c712","slug":"wcag-contrast-ratio","trashed":false,"description":"","likes":30,"publish_level":"public","forks":0,"fork_of":null,"has_importers":true,"update_time":"2019-11-21T22:09:24.826Z","first_public_version":null,"paused_version":null,"publish_time":"2019-11-21T22:09:24.826Z","publish_version":450,"latest_version":450,"thumbnail":"daef19584af887e4b841d023fc51e061fde0704a8483e97adeb43dc7abcfdd9b","default_thumbnail":"daef19584af887e4b841d023fc51e061fde0704a8483e97adeb43dc7abcfdd9b","roles":[],"sharing":null,"owner":{"id":"8c2e47252fd4995c","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","type":"team","tier":"starter_2024"},"creator":{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro"},"authors":[{"id":"074c414ad1d825f5","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","name":"Mike Bostock","login":"mbostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","tier":"pro","approved":true,"description":""}],"collections":[{"id":"9b855a35df5fa1ff","type":"public","slug":"color","title":"Color","description":"","update_time":"2018-10-21T16:28:01.819Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"daef19584af887e4b841d023fc51e061fde0704a8483e97adeb43dc7abcfdd9b","thumbnail":"daef19584af887e4b841d023fc51e061fde0704a8483e97adeb43dc7abcfdd9b","listing_count":8,"parent_collection_count":0,"owner":{"id":"8c2e47252fd4995c","avatar_url":"https://avatars.observableusercontent.com/avatar/82811927da99f8938001b2ef1f552ad2c47083e46ebc55a3a146a5a5848c4519","login":"mbostock","name":"Mike Bostock","bio":"Visualization toolmaker. Founder @observablehq. Creator @d3. Former @nytgraphics. Pronounced BOSS-tock.","home_url":"https://bost.ocks.org/mike/","type":"team","tier":"starter_2024"}}],"files":[],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":450,"title":"WCAG Contrast Ratio","license":null,"copyright":"","nodes":[{"id":245,"value":"md`# WCAG Contrast Ratio\n\nThe [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/TR/WCAG/) include convenient quantitative recommendations for making text and graphics accessible based on the minimum acceptable contrast of foreground against background.`","pinned":false,"mode":"js","data":null,"name":null},{"id":249,"value":"md`For example, <span style=\"background-color: yellow;\">black on yellow</span> has a high contrast ratio (${contrast(\"yellow\", \"black\").toFixed(2)}) and therefore should be easier to read, whereas <span style=\"background-color: steelblue; color: lightsteelblue;\">blue on blue</span> is low contrast (${contrast(\"steelblue\", \"lightsteelblue\").toFixed(2)}) and harder to read.`","pinned":false,"mode":"js","data":null,"name":null},{"id":0,"value":"md`The *contrast ratio* ${tex`C`} of two colors is defined as\n\n${tex.block`C = \\frac{\\max(L_1, L_2) + 0.05}{\\min(L_1, L_2) + 0.05}`}\n\nwhere ${tex`L_1`} and ${tex`L_2`} are the [relative luminances](https://en.wikipedia.org/wiki/Relative_luminance),\n\n${tex.block`L = 0.2126R + 0.7152G + 0.0722B,`}\n\nand ${tex`RGB`} are respectively the red, green and blue color channel values in “linear-light” RGB space. Given an sRGB value ${tex`V \\in [0,1]`}, the corresponding linear-light RGB value ${tex`v \\in [0,1]`} is\n\n${tex.block`\nv = \\begin{cases}\nV/12.92 & \\text{if } V \\leq 0.04045 \\\\\n((V + 0.055)/1.055)^{2.4} & \\text{otherwise.}\n\\end{cases}\n`}`","pinned":false,"mode":"js","data":null,"name":null},{"id":311,"value":"md`This notebook implements the WCAG contrast ratio so that you can see how it works, and also see if two given colors meet the WCAG success criteria.`","pinned":false,"mode":"js","data":null,"name":null},{"id":5,"value":"viewof colors = {\n  const form = html`<form style=\"font: 12px var(--sans-serif);\">\n  <label style=\"display: block;\"><input name=ib type=color value=\"#343e58\">\n  <span style=\"margin-left: 0.2em;\"><output name=ob></output> = background</span></label>\n  <label style=\"display: block;\"><input name=if type=color value=\"#ca6c87\">\n  <span style=\"margin-left: 0.2em;\"><output name=of></output> = foreground</span></label>\n</form>`;\n  form.oninput = () => {\n    form.value = {foreground: form.if.value, background: form.ib.value};\n    form.of.value = form.if.value;\n    form.ob.value = form.ib.value;\n  };\n  form.oninput();\n  return form;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":77,"value":"html`<div contentEditable style=\"outline: none; background-color: ${colors.background}; color: ${colors.foreground}; padding-top: 16px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 24px;\">\n  This is large sample text.\n</div><div contentEditable style=\"outline: none; background-color: ${colors.background}; color: ${colors.foreground}; padding-bottom: 16px; display: flex; align-items: center; justify-content: center;\">\n  And this is normal text.</div>`","pinned":false,"mode":"js","data":null,"name":null},{"id":393,"value":"md`The contrast ratio ${tex`C = \\frac{${Math.max(l1, l2).toFixed(3)}\\,\\textcolor{${l1 > l2 ? colors.background : colors.foreground}}{\\rule{4px}{4px}}\\, + 0.05}{${Math.min(l1, l2).toFixed(3)}\\,\\textcolor{${l1 > l2 ? colors.foreground : colors.background}}{\\rule{4px}{4px}}\\, + 0.05} = ${c.toFixed(2)}`} for these two colors.`","pinned":false,"mode":"js","data":null,"name":null},{"id":91,"value":"html`<table>\n  <thead>\n    <tr>\n      <th style=\"width: 5em;\"></th>\n      <th>Level</th>\n      <th>Content</th>\n      <th>Threshold</th>\n    </tr>\n  </thead>\n  <tbody>\n    ${criteria.map(test => html`<tr>\n      <th style=\"text-align: right; padding-right: 0.3em; ${c >= test.value ? \"\" : \"color: red;\"};\">${c >= test.value ? \"pass 👍\" : \"fail 👎\"}</th>\n      <td>${test.level}</td>\n      <td>${test.content}</td>\n      <td>${test.value.toFixed(1)}</td>\n    </tr>`)}\n  </tbody>\n</table>`","pinned":false,"mode":"js","data":null,"name":null},{"id":355,"value":"md`---\n\n## Implementation\n\nFirst, convert the two colors from sRGB to linear-light RGB.`","pinned":false,"mode":"js","data":null,"name":null},{"id":63,"value":"c1 = rgb_lrgb(d3.rgb(colors.background))","pinned":true,"mode":"js","data":null,"name":null},{"id":264,"value":"c2 = rgb_lrgb(d3.rgb(colors.foreground))","pinned":true,"mode":"js","data":null,"name":null},{"id":15,"value":"function rgb_lrgb({r, g, b}) {\n  return [r / 255, g / 255, b / 255].map(rgb_lrgb1);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":26,"value":"function rgb_lrgb1(v) {\n  return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":280,"value":"md`Next compute the relative luminances ${tex`L_1`} and ${tex`L_2`}.`","pinned":false,"mode":"js","data":null,"name":null},{"id":266,"value":"l1 = lrgb_luminance(c1)","pinned":true,"mode":"js","data":null,"name":null},{"id":272,"value":"l2 = lrgb_luminance(c2)","pinned":true,"mode":"js","data":null,"name":null},{"id":33,"value":"function lrgb_luminance([r, g, b]) {\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":282,"value":"md`And lastly, the contrast ratio ${tex`C`}.`","pinned":false,"mode":"js","data":null,"name":null},{"id":44,"value":"c = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)","pinned":true,"mode":"js","data":null,"name":null},{"id":292,"value":"md`Here’s the whole thing as a function, for convenience.`","pinned":false,"mode":"js","data":null,"name":null},{"id":294,"value":"function contrast(c1, c2) {\n  const l1 = lrgb_luminance(rgb_lrgb(d3.rgb(c1)));\n  const l2 = lrgb_luminance(rgb_lrgb(d3.rgb(c2)));\n  return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":390,"value":"contrast(\"yellow\", \"blue\")","pinned":true,"mode":"js","data":null,"name":null},{"id":284,"value":"md`And here are the WCAG success criteria for contrast.`","pinned":false,"mode":"js","data":null,"name":null},{"id":162,"value":"criteria = [\n  {level: \"AAA\", content: \"normal text\", value: 7.0},\n  {level: \"AAA\", content: \"large text\", value: 4.5},\n  {level: \"AA\", content: \"normal text\", value: 4.5},\n  {level: \"AA\", content: \"large text\", value: 3.0},\n  {level: \"AA\", content: \"graphics\", value: 3.0}\n]","pinned":true,"mode":"js","data":null,"name":null},{"id":286,"value":"md`---\n\n## Appendix\n\nWe use [d3-color](https://github.com/d3/d3-color) to parse colors.`","pinned":false,"mode":"js","data":null,"name":null},{"id":253,"value":"d3 = require(\"d3-color@1\")","pinned":true,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}