├── .gitignore ├── LICENSE.txt ├── README.md ├── package-lock.json ├── package.json ├── scripts └── create-briefings.js └── src ├── assets ├── briefing.css ├── fonts │ ├── IBMPlexSans-Regular.ttf │ └── IBMPlexSans-SemiBold.ttf ├── imgs │ ├── browser-focus.png │ ├── control-states.png │ ├── logo.png │ ├── monstera.png │ ├── mozart-1.jpg │ ├── mozart-2.jpg │ ├── piano-1.jpg │ └── soundcloud.jpg ├── index.css ├── normalize.css ├── polyfill-focus-visible.js ├── polyfill-inert.js ├── prism.css ├── prism.js ├── reset.css └── theme.css ├── briefings ├── 1.1.html ├── 1.1.md ├── 1.2.html ├── 1.2.md ├── 1.3.html ├── 1.3.md ├── 2.1.html ├── 2.1.md ├── 2.2.html ├── 2.2.md ├── 2.3.html ├── 2.3.md ├── 3.1.html ├── 3.1.md ├── 3.2.html ├── 3.2.md ├── 3.3.html ├── 3.3.md ├── 4.1.html ├── 4.1.md ├── 4.2.html ├── 4.2.md ├── 4.3.html └── 4.3.md ├── exercises ├── 1.1.css ├── 1.1.html ├── 1.2.css ├── 1.2.html ├── 1.3.css ├── 1.3.html ├── 2.1.css ├── 2.1.html ├── 2.2.css ├── 2.2.html ├── 2.3.css ├── 2.3.html ├── 3.1.html ├── 3.2.html ├── 3.3-short.html ├── 3.3.html ├── 4.1.css ├── 4.1.html ├── 4.1.react.html ├── 4.1.react.js ├── 4.2.css ├── 4.2.html ├── 9_archive.html └── 9_arhive.css ├── favicon.ico ├── index.html └── solutions ├── 1.1.css ├── 1.1.html ├── 1.2.css ├── 1.2.html ├── 1.3.css ├── 1.3.html ├── 2.1.css ├── 2.1.html ├── 2.2.css ├── 2.2.html ├── 2.3.css ├── 2.3.html ├── 3.1.html ├── 3.2.html ├── 3.3-short.html ├── 3.3.html ├── 3.3_demo_exercise.mp4 ├── 3.3_demo_solution.mp4 ├── 4.1.css ├── 4.1.html ├── 4.1.react.html ├── 4.1.react.js ├── 4.2.css └── 4.2.html /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .vscode/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Sandrina Pereira, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshop-a11y-fundamentals", 3 | "private": true, 4 | "title": "Web Accessibility Fundamentals", 5 | "version": "1.1.0", 6 | "description": "In this workshop, we'll explore every common accessibility no-nos and learn how to make them work properly for everyone using a mouse or a keyboard.", 7 | "main": "src/index.html", 8 | "author": "@sandrina-p", 9 | "license": "GPL-3.0-only", 10 | "scripts": { 11 | "start": "live-server src --port=5000", 12 | "write": "nodemon ./scripts/create-briefings.js --watch ./ -e js,md" 13 | }, 14 | "devDependencies": { 15 | "fast-glob": "^3.2.2", 16 | "live-server": "^1.2.1", 17 | "nodemon": "^2.0.7", 18 | "showdown": "^1.9.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/create-briefings.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const glob = require("fast-glob"); 3 | const path = require("path"); 4 | const showdown = require("showdown"); 5 | 6 | const converter = new showdown.Converter(); 7 | 8 | const workshopTitle = "A11Y+Fundamentals"; 9 | const briefFiles = glob.sync("./src/briefings/*.md"); 10 | 11 | briefFiles.forEach((brief) => { 12 | const briefName = path.basename(brief, ".md"); 13 | const content = fs.readFileSync(brief, "utf8"); 14 | const html = converter.makeHtml(content); 15 | 16 | fs.writeFileSync( 17 | `src/briefings/${briefName}.html`, 18 | ` 19 | 20 | 21 | 22 | Briefing #${briefName} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 |
36 | 37 | ${html} 38 | 39 |
40 | 48 | 49 | 50 | 51 | 52 | 53 | `, 54 | (err) => { 55 | if (err) return console.log(err); 56 | } 57 | ); 58 | }); 59 | console.log(`:: ✅ All ${briefFiles.length} briefings created!`); 60 | -------------------------------------------------------------------------------- /src/assets/briefing.css: -------------------------------------------------------------------------------- 1 | main { 2 | margin: 0 auto; 3 | max-width: var(--theme-width); 4 | padding: 40px 16px 16px; 5 | } 6 | 7 | a { 8 | display: inline-block; 9 | color: inherit; 10 | text-decoration-color: var(--theme-primary); 11 | line-height: 1.2; 12 | border-radius: 3px; 13 | } 14 | 15 | ul a { 16 | display: inline; 17 | } 18 | 19 | a:hover { 20 | color: var(--theme-primary); 21 | } 22 | 23 | a:focus { 24 | background-color: var(--theme-bg_1); 25 | box-shadow: var(--theme-focus_shadow); 26 | outline: var(--theme-focus_outline); 27 | } 28 | 29 | pre { 30 | background: var(--theme-bg_1); 31 | display: block; 32 | padding: 16px; 33 | color: inherit; 34 | border-radius: 3px; 35 | } 36 | 37 | code[class*="language-"], 38 | pre[class*="language-"] { 39 | /* override prims */ 40 | font-size: 1.2rem; 41 | } 42 | 43 | pre code { 44 | background: var(--theme-bg_1); 45 | } 46 | 47 | code { 48 | background: #e7e1dc; 49 | padding: 1px 3px; 50 | font-weight: 100; 51 | } 52 | 53 | h1 { 54 | font-size: 3.2rem; 55 | font-weight: 600; 56 | color: var(--theme-primary); 57 | margin: 0 0 16px; 58 | line-height: 1.2; 59 | } 60 | 61 | h2 { 62 | font-size: 2.4rem; 63 | font-weight: 600; 64 | margin: 32px 0 16px; 65 | line-height: 1.2; 66 | background: var(--theme-primary_smooth); 67 | padding: 2px 8px; 68 | border-radius: 4px; 69 | } 70 | 71 | h3 { 72 | font-size: 2rem; 73 | font-weight: 600; 74 | margin-top: 32px; 75 | margin-bottom: 0; 76 | line-height: 1.2; 77 | } 78 | 79 | h4 { 80 | font-size: 1.8rem; 81 | font-weight: 600; 82 | margin: 16px 0 -10px; 83 | line-height: 1.2; 84 | } 85 | 86 | hr { 87 | margin: 32px 0 0; 88 | border: none; 89 | border-bottom: 1px solid var(--theme-primary_smooth); 90 | } 91 | 92 | summary { 93 | margin-bottom: 4px; 94 | } 95 | 96 | blockquote { 97 | font-style: italic; 98 | opacity: 0.7; 99 | margin-left: 8px; 100 | padding-left: 8px; 101 | border-left: 1px solid var(--theme-text_1); 102 | } 103 | 104 | .bfg-hotlinks { 105 | position: fixed; 106 | top: 0; 107 | width: 100%; 108 | display: flex; 109 | justify-content: space-between; 110 | } 111 | 112 | .bfg-hotlinks a { 113 | background: var(--theme-bg_1); 114 | border: 1px solid var(--theme-primary); 115 | border-radius: 4px; 116 | padding: 4px 16px; 117 | text-decoration: none; 118 | margin: 6px; 119 | } 120 | 121 | .bfg-hotlinks a:focus { 122 | box-shadow: var(--theme-focus_shadow); 123 | outline: var(--theme-focus_outline); 124 | } 125 | -------------------------------------------------------------------------------- /src/assets/fonts/IBMPlexSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/fonts/IBMPlexSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/IBMPlexSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/fonts/IBMPlexSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/imgs/browser-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/browser-focus.png -------------------------------------------------------------------------------- /src/assets/imgs/control-states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/control-states.png -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/monstera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/monstera.png -------------------------------------------------------------------------------- /src/assets/imgs/mozart-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/mozart-1.jpg -------------------------------------------------------------------------------- /src/assets/imgs/mozart-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/mozart-2.jpg -------------------------------------------------------------------------------- /src/assets/imgs/piano-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/piano-1.jpg -------------------------------------------------------------------------------- /src/assets/imgs/soundcloud.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/assets/imgs/soundcloud.jpg -------------------------------------------------------------------------------- /src/assets/index.css: -------------------------------------------------------------------------------- 1 | ul, 2 | ol { 3 | list-style-type: none; 4 | padding: 0; 5 | margin: 0; 6 | } 7 | 8 | li::before { 9 | position: absolute; 10 | content: "\200B"; /* add zero-width space */ 11 | } 12 | 13 | .content { 14 | max-width: var(--theme-width); 15 | } 16 | 17 | .header { 18 | padding-top: 40px; 19 | margin-bottom: 32px; 20 | } 21 | 22 | .header-title { 23 | line-height: 1.2; 24 | margin-bottom: 8px; 25 | } 26 | 27 | .header-txt { 28 | margin: 0; 29 | color: var(--theme-text_1); 30 | } 31 | 32 | .bannerJoin { 33 | text-align: center; 34 | align-items: center; 35 | margin: 0 0 32px; 36 | } 37 | 38 | .bannerJoin p { 39 | margin: 0; 40 | } 41 | 42 | .bannerJoin p:last-of-type { 43 | margin-bottom: 12px; 44 | } 45 | 46 | .bannerJoin .btn { 47 | animation: attention 3s infinite; 48 | } 49 | 50 | @keyframes attention { 51 | 25%, 52 | 75% { 53 | filter: saturate(1); 54 | } 55 | 56 | 50% { 57 | filter: saturate(2); 58 | } 59 | } 60 | 61 | @media (prefers-reduced-motion: no-preference) { 62 | .bannerJoin .btn { 63 | animation-iteration-count: 2; 64 | } 65 | 66 | @keyframes attention { 67 | 0%, 68 | 20%, 69 | 50%, 70 | 100% { 71 | transform: scale(1); 72 | } 73 | 74 | 30% { 75 | transform: scale(1.25); 76 | box-shadow: 0 0 15px hsl(266deg 100% 61% / 60%); 77 | } 78 | } 79 | } 80 | 81 | .bannerAlert { 82 | border: 5px dashed #e44545; 83 | font-size: 1.8rem; 84 | padding: 16px; 85 | margin: 32px 0; 86 | text-align: center; 87 | } 88 | 89 | .topic-item { 90 | margin-bottom: 32px; 91 | } 92 | 93 | .topic-title { 94 | margin-bottom: 16px; 95 | } 96 | 97 | .subTopic-item { 98 | background: var(--theme-bg_1); 99 | margin-bottom: 16px; 100 | padding: 20px 16px; 101 | box-shadow: 2px 2px var(--theme-primary_smooth); 102 | border-radius: 4px; 103 | } 104 | 105 | .subTopic-heading { 106 | margin: 0 0 8px; 107 | display: flex; 108 | align-items: baseline; 109 | } 110 | 111 | .subTopic-heading h4 { 112 | line-height: inherit; 113 | font-weight: inherit; 114 | font-size: inherit; 115 | margin: 0; 116 | } 117 | 118 | .subTopic-bullet { 119 | font-size: 0.8em; 120 | margin-right: 8px; 121 | color: var(--theme-primary); 122 | } 123 | 124 | .subTopic-link:not(:last-child) { 125 | margin-right: 24px; 126 | } 127 | -------------------------------------------------------------------------------- /src/assets/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.20.0 2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */ 3 | /** 4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 5 | * Based on https://github.com/chriskempson/tomorrow-theme 6 | * @author Rose Pritchard 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: #ccc; 12 | background: none; 13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 14 | font-size: 1em; 15 | text-align: left; 16 | white-space: pre; 17 | word-spacing: normal; 18 | word-break: normal; 19 | word-wrap: normal; 20 | line-height: 1.5; 21 | 22 | -moz-tab-size: 4; 23 | -o-tab-size: 4; 24 | tab-size: 4; 25 | 26 | -webkit-hyphens: none; 27 | -moz-hyphens: none; 28 | -ms-hyphens: none; 29 | hyphens: none; 30 | 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"] { 35 | padding: 1em; 36 | margin: .5em 0; 37 | overflow: auto; 38 | } 39 | 40 | :not(pre) > code[class*="language-"], 41 | pre[class*="language-"] { 42 | background: #2d2d2d; 43 | } 44 | 45 | /* Inline code */ 46 | :not(pre) > code[class*="language-"] { 47 | padding: .1em; 48 | border-radius: .3em; 49 | white-space: normal; 50 | } 51 | 52 | .token.comment, 53 | .token.block-comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: #999; 58 | } 59 | 60 | .token.punctuation { 61 | color: #ccc; 62 | } 63 | 64 | .token.tag, 65 | .token.attr-name, 66 | .token.namespace, 67 | .token.deleted { 68 | color: #e2777a; 69 | } 70 | 71 | .token.function-name { 72 | color: #6196cc; 73 | } 74 | 75 | .token.boolean, 76 | .token.number, 77 | .token.function { 78 | color: #f08d49; 79 | } 80 | 81 | .token.property, 82 | .token.class-name, 83 | .token.constant, 84 | .token.symbol { 85 | color: #f8c555; 86 | } 87 | 88 | .token.selector, 89 | .token.important, 90 | .token.atrule, 91 | .token.keyword, 92 | .token.builtin { 93 | color: #cc99cd; 94 | } 95 | 96 | .token.string, 97 | .token.char, 98 | .token.attr-value, 99 | .token.regex, 100 | .token.variable { 101 | color: #7ec699; 102 | } 103 | 104 | .token.operator, 105 | .token.entity, 106 | .token.url { 107 | color: #67cdcc; 108 | } 109 | 110 | .token.important, 111 | .token.bold { 112 | font-weight: bold; 113 | } 114 | .token.italic { 115 | font-style: italic; 116 | } 117 | 118 | .token.entity { 119 | cursor: help; 120 | } 121 | 122 | .token.inserted { 123 | color: green; 124 | } 125 | 126 | -------------------------------------------------------------------------------- /src/assets/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/briefings/1.1.md: -------------------------------------------------------------------------------- 1 | # 1.1 Semantic HTML 2 | 3 | ## Introduction 4 | 5 | Web standards are rules and guidelines used to promote code consistency when building a web page. With those, anyone (people and robots) can access and understand the content of a webpage in a meaningful way. 6 | 7 | One of the Web standards consists in writing semantic HTML markup: Go beyond `
` and `` (non-semantic elements) and replace them by [semantic elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element). Use them where it makes sense: to describe sections, text, media, and so on. This way, we are not only writing **readable code**, but we are also making the web a more **accessible place**. 8 | 9 | ## Exercise 10 | 11 | In the [exercise page](../exercises/1.1.html), 12 | there's a layout implemented that looks good but the semantics are a mess. 13 | 14 | People who read this page through an Assistive Technology (AT), for example, a [SR (screen reader)](https://developer.mozilla.org/en-US/docs/Glossary/Screen_reader), will struggle to understand and interact with the page. 15 | 16 | **🎯 Goal:** Improve the HTML by using [semantic elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Element). 17 | 18 | **Tip:** For now, we won't use a SR. Instead, we'll use an analogy. Let's imagine the page as if it was a book: plain old text. Remove the page styles and improve the HTML until this book makes sense to you. 19 | 20 | ## Bonus 21 | 22 | ### 1. Order of content 23 | 24 | In a book, the title is always the first thing to show up. However, on this page, the tag "On Sale" appears before the title. 25 | 26 | **🎯 Goal:** _reorder the HTML elements_ so that the title and the tag appear in the correct order, but without changing the design! 27 | 28 |
29 | 🍀 Toggle CSS hint #1 30 | 31 | Perhaps some flexbox trick can help us to _reverse_ the _order_ visually. 32 | 33 |
34 | 35 |
36 | 🍀 Toggle CSS hint #2 37 | 38 | With CSS flexbox there are 2 ways to display the tag before the title: 39 | 40 | - in `.header`, reverse the order with [`flex-direction: column-reverse;`](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction). 41 | - or, in `.header-tag` move it to the top with [`order: -1;`](https://developer.mozilla.org/en-US/docs/Web/CSS/order). 42 |
43 | 44 | ### 2. Missing content 45 | 46 | We can visually understand that the plant is on sale ($40 to $30). However, if we see the page naked (without styles), it doesn't make much sense. We see two price tags, but we don't know the difference between them. Some people may not even understand that's the plant price. 47 | 48 | **🎯 Goal:** Add the missing words to be visible when the page is displayed without styles. 49 | 50 | ```html 51 | Price: 52 | ``` 53 | 54 | **Hint:** What CSS will you use at `.visually-hidden`? There are better ways to hide content than using `display: none;`. Check how to [visually hide content](https://a11yproject.com/posts/how-to-hide-content/). 55 | 56 | ## Further reading 57 | 58 | - [Extension `HeadingsMap`](https://rumoroso.bitbucket.io/) - Chrome or Firefox 59 | - [Accessible heading structure](https://www.a11yproject.com/posts/how-to-accessible-heading-structure/) 60 | - [How to hide content](https://a11yproject.com/posts/how-to-hide-content/) 61 | - [HTML5 deep dive into article and section](https://www.smashingmagazine.com/2020/01/html5-article-section/) 62 | - Fun fact: on April 9 is the [CSS naked day](https://css-naked-day.github.io/) 63 | 64 | ### WCAG Success Criterion 65 | 66 | - [WCAG 1.3.2 Meaningful Sequence - Level A](https://www.w3.org/TR/WCAG21/#meaningful-sequence) 67 | - [WCAG 2.4.6 Headings and Labels - Level AA](https://www.w3.org/TR/WCAG21/#headings-and-labels) 68 | - [WCAG 2.4.10 Section Headings - Level AAA](https://www.w3.org/TR/WCAG21/#section-headings) 69 | 70 | ## Exercise takeaways 71 | 72 |
73 | (After the exercise) Reveal takeaways 74 | 75 | - Be more than a _div'eloper_. HTML tags provide meaning and default functionality out of the box. 76 | - The heading structure must prioritize the content of the page rather than the visual designs. 77 | - Use `.sr-only` to complement the missing visual content. 78 | - Aim to organize the DOM as a book — when necessary use CSS to tweak the visual order. 79 | - As a designer, include the "invisible headings" in your mockups. #invisibleCopy. 80 |
81 | -------------------------------------------------------------------------------- /src/briefings/1.3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Briefing #1.3 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 |

1.3. Meaningful images

21 |

Introduction

22 |

"One image is worth a thousand words", they say.

23 |

Images help us to understand better some content. They are especially useful for people with cognitive and learning disabilities.

24 |

However, inaccessible images can create major barriers for people with visual disabilities. Accessible images will benefit not only them but also the Search Engine Optimization (SEO) of a website.

25 |

We know that every <img> must have an alt attribute. However, it doesn't need to be used equally everywhere. 26 | In the same way, there are different images concepts, we can also describe them in different ways. 27 | For example, if the image doesn't add any relevant information about the content, we can just leave an empty alt: alt="".

28 |

Exercise

29 |

In the exercise page, 30 | there are 4 visual representations, but none of them are accessible.

31 |

🎯 Goal: Add the needed accessible descriptions to each image.

32 |

Tip 1: Alt text - Replace the image src with an invalid URL and see how the browser renders a broken image. Does the alt text make sense?

33 |

Bonus

34 |

In slow internet connections, images take time to load. Some people even prefer to disable images to save bandwidth data.

35 |

🎯 Goal: Edit the CSS to ensure the page looks good when images don't load.

36 |

🍀 Tip #1: In your daily job, take this as an opportunity for developers and designers to collaborate!

37 |

🍀 Tip #2: In real projects, we can block all images, like this:

38 |
    39 |
  1. In Chrome, go to DevTools > 3 dots (top right) > More tools > "Network request blocking".
  2. 40 |
  3. A new container in the bottom is opened.
  4. 41 |
  5. Add a new blocker: Click "+" button, write *.jpg and save it.
  6. 42 |
  7. Refresh the page and voilá. The images are not loaded!
  8. 43 |
44 |

Further reading

45 | 52 |

WCAG Success Criterion

53 | 57 |

Exercise takeaways

58 |

59 | (After the exercise) Reveal takeaways

60 |
    61 |
  • Empty alt (alt="") is always better than an unexistent alt.
  • 62 |
  • Alt is meant to be read by people, NotARobotImage.
  • 63 |
  • As a designer, go beyond the ideal state — Include design skeletons to be shown when the images are loading or broken.
  • 64 |
  • As a designer, include the images alts in your mockups. #invisibleCopy. 65 |
66 | 67 | 68 |
69 |
70 | 71 |
72 |

73 | Finished on time? Help your group or give feedback about this exercise. 74 |

75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/briefings/1.3.md: -------------------------------------------------------------------------------- 1 | # 1.3. Meaningful images 2 | 3 | ## Introduction 4 | 5 | _"One image is worth a thousand words"_, they say. 6 | 7 | Images help us to understand better some content. They are especially useful for people with cognitive and learning disabilities. 8 | 9 | However, inaccessible images can create major barriers for people with visual disabilities. Accessible images will benefit not only them but also the Search Engine Optimization (SEO) of a website. 10 | 11 | We know that every `` must have an `alt` attribute. However, it doesn't need to be used equally everywhere. 12 | In the same way, there are [different images concepts](https://www.w3.org/WAI/tutorials/images), we can also describe them in different ways. 13 | For example, if the image doesn't add any relevant information about the content, we can just leave an empty alt: `alt=""`. 14 | 15 | ## Exercise 16 | 17 | In the [exercise page](../exercises/1.3.html), 18 | there are 4 visual representations, but none of them are accessible. 19 | 20 | **🎯 Goal:** Add the needed accessible descriptions to each image. 21 | 22 | **Tip 1: Alt text** - Replace the image `src` with an invalid URL and see how the browser renders a broken image. Does the alt text make sense? 23 | 24 | ### Bonus 25 | 26 | In slow internet connections, images take time to load. Some people even prefer to disable images to save bandwidth data. 27 | 28 | **🎯 Goal:** Edit the CSS to ensure the page looks good when images don't load. 29 | 30 | **🍀 Tip #1:** In your daily job, take this as an opportunity for developers and designers to collaborate! 31 | 32 | **🍀 Tip #2**: In real projects, we can block all images, like this: 33 | 34 | 1. In Chrome, go to DevTools > 3 dots (top right) > More tools > "Network request blocking". 35 | 2. A new container in the bottom is opened. 36 | 3. Add a new blocker: Click "+" button, write `*.jpg` and save it. 37 | 4. Refresh the page and voilá. The images are not loaded! 38 | 39 | ## Further reading 40 | 41 | - [Images concepts](https://www.w3.org/WAI/tutorials/images) 42 | - [Alt usage - Decision tree](https://www.w3.org/wai/tutorials/images/decision-tree/) 43 | - [Writing great alt text: Emotion matters](https://jakearchibald.com/2021/great-alt-text/) 44 | - [Pros and cons of alt text](https://twitter.com/thingskatedid/status/1360331792067166208?s=20) 45 | - [Use CSS to detect invalid alt](https://twitter.com/geoffreycrofte/status/1274652138854121474?s=21) 46 | 47 | ### WCAG Success Criterion 48 | 49 | - [WCAG 1.1.1 Non-text content - Level A](https://www.w3.org/TR/WCAG21/#non-text-content) 50 | - [WCAG 1.4.5 Images of text - Level AA](https://www.w3.org/TR/WCAG21/#images-of-text) 51 | 52 | ## Exercise takeaways 53 | 54 |
55 | (After the exercise) Reveal takeaways 56 | 57 | - Empty alt (`alt=""`) is always better than an unexistent alt. 58 | - Alt is meant to be read by people, _NotARobotImage_. 59 | - As a designer, go beyond the ideal state — Include design skeletons to be shown when the images are loading or broken. 60 | - As a designer, include the images alts in your mockups. #invisibleCopy. 61 |
62 | -------------------------------------------------------------------------------- /src/briefings/2.2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Briefing #2.2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 |

2.2. Hidden elements

21 |

Introduction

22 |

Nowadays, a lot of content is hidden on a page until it's necessary: menus, drawers, dialogs, tooltips… you name it!

23 |

When we hide content visually, we must remember to hide it completely. Otherwise, those elements are still accessible, even when invisible.

24 |

This mistake can confuse and frustrate those who lose track of the focus indicator while navigating the page.

25 |

These are common ways to hide content:

26 |
/* Visually hidden but accessible by keyboard */
 27 | opacity: 0;
 28 | 
 29 | /* Completely hidden: visually and by keyboard */
 30 | display: none;
 31 | visibility: hidden;
 32 | 
33 |
<!-- Even invisible, "Get started" can be clicked / focused -->
 34 | <div style="opacity: 0">
 35 |   <button>Get started</button>
 36 | </div>
 37 | 
38 |

To completely remove the interaction, we need to use the attribute inert. This tells the browser to "ignore" the elements inside as if they didn't exist.

39 |
<!-- With `inert`, we cannot interact with the child button -->
 40 | <div style="opacity: 0" inert="true">
 41 |   <button>Get started</button>
 42 | </div>
 43 | 
44 |

Exercise

45 |

In the exercise page, 46 | there's a sidebar that can be toggled through a <button>. Use your keyboard to navigate the page. You'll notice the links inside the sidebar are still accessible even if the menu is hidden.

47 |

🎯 Goal: Make the interactive elements only accessible when visible.

48 |

Hint 1: Use Accessibility Insights "Tab Stops" feature to detect errors within interactive elements.

49 |

Hint 2: The attribute inert lacks browser support, so we need the inert polifyll.

50 |
<!-- Include the inert polyfill at the bottom of the page -->
 51 | <script src="../assets/polyfill-inert.js"></script>
 52 | 
53 |

Bonus

54 |

1. CSS transitions

55 |

This bonus is not about A11Y, it's about CSS transitions. I highly recommend you to pair with your group to explore this one!

56 |

The text "Here's a random article." has a fade-in/fade-out transition done with CSS opacity and transition.

57 |

Assuming you solved the A11Y exercise correctly, you've added visibility:hidden; to the element when it's hidden. But wait… now the fade-out transition is gone!

58 |

Goal: Ensure the fade-in and fade-out transitions work as before. This can be solved with only CSS.

59 |

Hint: In Chrome, go to "More tools > Animations". This panel helps you to inspect animations and find a solution.

60 |

61 | 🍀 Toggle CSS hint #1 62 | CSS transitions have multiple properties. We can use them to better control how the transition happens. 63 |

64 |

65 | 🍀 Toggle CSS hint #2 66 | Check transition-delay. With that we can better control when the visibility happens. 67 |

68 |

Further reading

69 | 75 |

WCAG Success Criterion

76 | 80 |

Exercise takeaways

81 |

82 | (After the exercise) Reveal takeaways

83 |
    84 |
  • To hide content completely, use display: none.
  • 85 |
  • To hide content just visually, use .sr-only instead of opacity: 0 to ensure all assistive technologies can access it.
  • 86 |
  • Use inert polyfill with caution, as it can cause performance issues in complex DOM trees. 87 |
88 | 89 | 90 |
91 |
92 | 93 |
94 |

95 | Finished on time? Help your group or give feedback about this exercise. 96 |

97 |
98 |
99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/briefings/2.2.md: -------------------------------------------------------------------------------- 1 | # 2.2. Hidden elements 2 | 3 | ## Introduction 4 | 5 | Nowadays, a lot of content is hidden on a page until it's necessary: menus, drawers, dialogs, tooltips... you name it! 6 | 7 | When we hide content visually, we must remember to hide it completely. Otherwise, those elements are still accessible, even when invisible. 8 | 9 | This mistake can confuse and frustrate those who lose track of the focus indicator while navigating the page. 10 | 11 | These are common ways to hide content: 12 | 13 | ```css 14 | /* Visually hidden but accessible by keyboard */ 15 | opacity: 0; 16 | 17 | /* Completely hidden: visually and by keyboard */ 18 | display: none; 19 | visibility: hidden; 20 | ``` 21 | 22 | ```html 23 | 24 |
25 | 26 |
27 | ``` 28 | 29 | To completely remove the interaction, we need to use the attribute `inert`. This tells the browser to "ignore" the elements inside as if they didn't exist. 30 | 31 | ```html 32 | 33 |
34 | 35 |
36 | ``` 37 | 38 | ## Exercise 39 | 40 | In the [exercise page](../exercises/2.2.html), 41 | there's a sidebar that can be toggled through a ` 18 | 19 | ``` 20 | 21 | In this case the element name does not make sense semantically. That's where ARIA (Accessible Rich Internet Applications) attributes come in. **ARIA allows us to enhance the HTML semantics of an element.** 22 | 23 | A common [naming technique](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/) is to add the attribute `aria-label`. 24 | 25 | ```html 26 | 27 | 28 | 29 | ``` 30 | 31 | Some automated translation tools will miss `aria-label`. A safer solution is to add a visually hidden element and hide the visual element. 32 | 33 | ```html 34 | 35 | 39 | 40 | 41 | ``` 42 | 43 | Note that **ARIA never modifies an element styling or behavior**. Check the next example: 44 | 45 | ```html 46 | 47 | Get started 48 | ``` 49 | 50 | This "visual button" is still not focusable and doesn't have keyboard event listeners. (Enter and Space keys). Whenever possible, prefer to use the native HTML elements and use ARIA only has an enhancement. 51 | 52 | There are [more than 45 `aria-*` attributes](https://www.w3.org/TR/wai-aria-1.1/#state_prop_def). Today we'll explore some of the most common. 53 | 54 | ## Exercise 55 | 56 | In the [exercise page](../exercises/3.1.html), 57 | there are multiple cases where some pieces of information were forgotten or misused. Some `aria-*` attributes and `.sr-only` class will be your friends here. 58 | 59 | **🎯 Goal:** Add accessible names or status to all elements. 60 | 61 | **Hint:** Use the Accessibility tab on DevTools to verify if the accessible name is working. It can be an alternative to screen readers. 62 | 63 | ## Further reading 64 | 65 | - [First rule of ARIA use: don't use ARIA](https://w3c.github.io/using-aria/#rule1) 66 | - [A11YSupport](https://a11ysupport.io/) - Like caniuse but for ARIA 67 | - [Translations to aria-\* attributes](https://adrianroselli.com/2019/11/aria-label-does-not-translate.html) 68 | - [Using `aria-current`](https://tink.uk/using-the-aria-current-attribute/) 69 | 70 | ### WCAG Success Criterion 71 | 72 | - [WCAG 4.1.2 Name, Role, Value - Level A](https://www.w3.org/TR/WCAG21/#name-role-value) 73 | - [WCAG 3.1.6 Pronunciation - Level AAA](https://www.w3.org/TR/WCAG21/#focus-visible) 74 | - [WCAG 2.4.8 Link Purpose - Level A](https://www.w3.org/TR/WCAG21/#link-purpose-in-context) 75 | - [WCAG 2.4.9 Link Purpose (Link Only) - Level AAA](https://www.w3.org/TR/WCAG21/#link-purpose-link-only) 76 | 77 | ## Exercise takeaways 78 | 79 |
80 | (After the exercise) Reveal takeaways 81 | 82 | - Use `ARIA` as last resource. Prefer HTML elements whenever possible. 83 | - When creating an element, always ask yourself "Are the name, role and state accessibles?" 84 | - Use a Screen Reader when testing new user interfaces. 85 | - Ensure the content is meaningful when accessed without colors, styles, or images. 86 | - As as designer, provide the "name" of all interactive elements in your mockups. #invisibleCopy 87 |
88 | -------------------------------------------------------------------------------- /src/briefings/3.2.md: -------------------------------------------------------------------------------- 1 | # 3.2. Page Landmarks 2 | 3 | ## Introduction 4 | 5 | Here's a myth: _"People using screen readers read all the text."_ Not true. We are all the same: we scan the page looking for keywords until something catches our attention. Only then, we start to actually read it. 6 | 7 | Screen Readers users usually navigate the page through _headings_ and _landmarks_. We already know the importance of a solid heading structure. Now let's talk about landmarks. 8 | 9 | Landmarks identify sections of a page, which allows SR users to orient themselves in the page and jump directly to them. 10 | 11 | Some elements are landmarks by default, such as `
` and `
62 | 63 |
64 | 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/exercises/1.3.css: -------------------------------------------------------------------------------- 1 | /* ---------------------------------------- *\ 2 | |* You do not need to touch this file *| 3 | |* but you can see how it was made. *| 4 | \** ---------------------------------------- */ 5 | 6 | /* Case 1 */ 7 | .case1 { 8 | position: relative; 9 | width: 100%; 10 | padding: 0; 11 | } 12 | 13 | .case1-img { 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | object-fit: cover; 20 | } 21 | 22 | .case1-text { 23 | position: relative; 24 | margin: 0; 25 | padding: 40px 24px; 26 | font-size: 3.2rem; 27 | color: white; 28 | text-shadow: 1px 1px 3px #000; 29 | } 30 | 31 | /* Case 2 */ 32 | .case2 { 33 | display: flex; 34 | } 35 | 36 | .case2 img { 37 | width: 48px; 38 | height: 48px; 39 | border-radius: 50%; 40 | } 41 | 42 | .case2-content { 43 | margin-left: 16px; 44 | } 45 | 46 | .case2-name { 47 | display: block; 48 | font-weight: 600; 49 | margin-bottom: 4px; 50 | } 51 | 52 | .case2-quote { 53 | font-style: italic; 54 | } 55 | 56 | .case2-quote::before, 57 | .case2-quote::after { 58 | content: ""; 59 | } 60 | 61 | /* Case 3 */ 62 | .case3 { 63 | max-width: 250px; 64 | border: 1px solid #6f6f6f; 65 | padding: 16px; 66 | } 67 | 68 | .case3 figure { 69 | margin: 0; 70 | } 71 | 72 | .case3-img { 73 | width: 100%; 74 | display: block; 75 | } 76 | 77 | .case3-caption { 78 | font-style: italic; 79 | font-size: 1.4rem; 80 | margin-top: 4px; 81 | } 82 | 83 | /* Case 4 */ 84 | .case4 { 85 | display: flex; 86 | align-items: center; 87 | } 88 | 89 | .case4-text { 90 | margin-left: 4px; 91 | } 92 | -------------------------------------------------------------------------------- /src/exercises/1.3.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Exercise #1.3 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |

Meaningful images

26 | 27 | 28 | 29 |
30 | 31 |

Start your piano classes today!

32 |
33 | 34 | 35 | 36 | 37 |
38 | Mozart avatar 39 |
40 | Wolfgang Amadeus Mozart 41 | 42 | The music is not in the notes, but in the silence between. 43 | 44 |
45 |
46 | 47 | 48 | 49 |
50 |
51 | 52 |
53 | Anonymous portrait of the child Mozart, possibly by Pietro Antonio Lorenzoni; painted in 1763. 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | soundclound logo 62 | Classical Music Playlist 63 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /src/exercises/2.1.css: -------------------------------------------------------------------------------- 1 | /* Never ever do this 🥺 2 | 💡 Delete this declaration! */ 3 | *:focus { 4 | outline: none; 5 | } 6 | 7 | /* ------- BUTTON ------- */ 8 | 9 | .c-btn { 10 | display: inline-flex; 11 | align-items: center; 12 | justify-content: center; 13 | font-size: 1.8rem; 14 | border: none; 15 | padding: 8px 24px; 16 | background-color: var(--theme-primary); 17 | color: #fff; 18 | border: none; 19 | cursor: pointer; 20 | border-radius: 4px; 21 | } 22 | 23 | .c-btnPrimary:hover { 24 | opacity: 0.8; 25 | } 26 | 27 | /* 💡 Add the button styles here 28 | - How does the input looks like when disabled or :focus? Try it out! 29 | - Suggestion: You can add a nice box-shadow on focus: 30 | box-shadow: var(--theme-focus_shadow); 31 | */ 32 | 33 | /* ------- LINK ------- */ 34 | 35 | .c-link { 36 | all: unset; 37 | color: var(--theme-text_0); 38 | text-decoration: underline; 39 | cursor: pointer; 40 | } 41 | 42 | .c-link:hover { 43 | color: var(--theme-primary); 44 | } 45 | 46 | /* 💡 Add link focus styles here 47 | - You could change the background or add a border (shadow) */ 48 | 49 | /* ------- TEXT INPUT ------- */ 50 | 51 | .fieldArea { 52 | max-width: 200px; 53 | } 54 | 55 | .fieldLabel { 56 | display: block; 57 | font-weight: 600; 58 | /* 💡 Bonus #3 Margins are not included 59 | in the interactive area, but paddings are! */ 60 | margin-bottom: 8px; 61 | } 62 | 63 | .textInput { 64 | width: 100%; 65 | height: 32px; 66 | padding: 4px 8px; 67 | border-radius: 4px; 68 | border: 1px solid var(--theme-text_1); 69 | } 70 | 71 | /* 💡 Input focus styles */ 72 | 73 | .textInput:hover { 74 | border-color: var(--theme-primary); 75 | } 76 | 77 | /* 💡 Improve the focus indicator with something more visible */ 78 | .textInput:focus { 79 | /* ... */ 80 | } 81 | 82 | /* ----- */ 83 | 84 | /* 💡 Bonus #3 85 | - You can use var(--theme-primary) for the color */ 86 | 87 | /* ------- BUTTON CLOSE ------- */ 88 | 89 | .c-btnCorner { 90 | all: unset; 91 | position: absolute; 92 | --cornerOffset: 16px; 93 | bottom: var(--cornerOffset); 94 | right: var(--cornerOffset); 95 | } 96 | 97 | .c-btnCorner-area { 98 | position: relative; 99 | width: 20px; 100 | height: 20px; 101 | border-radius: 50%; 102 | color: var(--theme-text_0); 103 | border: 1px solid; 104 | display: flex; 105 | align-items: center; 106 | justify-content: center; 107 | line-height: 1; 108 | cursor: pointer; 109 | } 110 | 111 | /* 💡 The hover indicator has enough contrast, 112 | so the focus can be exactly the same */ 113 | .c-btnCorner-area:hover { 114 | background-color: var(--theme-primary); 115 | color: var(--theme-bg_0); 116 | transition: background-color 150ms, color 150ms; 117 | } 118 | 119 | .c-btnCorner svg { 120 | width: 9px; 121 | margin-left: 1px; 122 | fill: currentColor; 123 | } 124 | -------------------------------------------------------------------------------- /src/exercises/2.1.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Exercise #2.1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |

Interactive elements

23 | 24 |
25 | 26 |
27 | 28 | 31 |
32 | 33 | 34 |
35 |

36 | If you are lost, you can get back 37 | 41 | 42 | and start over. 43 |

44 | 45 | 52 |
53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 75 |
76 |
77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/exercises/2.2.css: -------------------------------------------------------------------------------- 1 | /* ------- CASE #2 Custom input ------- */ 2 | 3 | .c-checkbox { 4 | position: relative; 5 | display: inline-block; 6 | cursor: pointer; 7 | margin-right: 15px; 8 | font-size: 1.4rem; 9 | } 10 | 11 | .c-checkbox input { 12 | /* 💡 We shouldn't use display:none, right? */ 13 | display: none; 14 | } 15 | 16 | /* ------- CASE #3 Toggle content ------- */ 17 | 18 | .coolText { 19 | /* 💡 It's visually hidden, but we need to hide it completely */ 20 | opacity: 0; 21 | padding: 16px 0 0 16px; 22 | transition: opacity 250ms; 23 | } 24 | 25 | .coolText.isVisible { 26 | opacity: 1; 27 | } 28 | 29 | /* ----------------------------------------- *\ 30 | |* You do not need to touch the code below *| 31 | |* but you can see how it was made. *| 32 | \** ----------------------------------------- */ 33 | 34 | /* ------- HEADER + MENU ------- */ 35 | 36 | .headerFixed { 37 | position: fixed; 38 | width: 100%; 39 | display: flex; 40 | justify-content: flex-end; 41 | padding: 8px; 42 | z-index: 2; 43 | } 44 | 45 | .menu { 46 | position: fixed; 47 | top: 0; 48 | right: 0; 49 | z-index: -1; 50 | width: 150px; 51 | height: 100%; 52 | padding-top: 50px; 53 | background: var(--theme-bg_1); 54 | border-left: 3px solid var(--theme-primary); 55 | transform: translateX(100%); 56 | transition: transform 300ms; 57 | } 58 | 59 | .menu.isVisible { 60 | transform: translateX(0%); 61 | } 62 | 63 | .menu-btn { 64 | height: 30px; 65 | min-width: auto; 66 | min-height: auto; 67 | line-height: 30px; 68 | padding-top: 0; 69 | padding-bottom: 0; 70 | } 71 | 72 | .menu a { 73 | display: block; 74 | padding: 4px 0; 75 | } 76 | 77 | /* ------- CHECKBOX ------- */ 78 | 79 | .c-checkbox:hover + :last-child::before { 80 | border-color: #6f6f6f; 81 | } 82 | 83 | .c-checkbox input:focus + :last-child::before { 84 | border-color: #8c00ff; 85 | box-shadow: var(--theme-focus_shadow); 86 | outline: var(--theme-focus_outline); 87 | } 88 | 89 | .c-checkbox:last-child::before, 90 | .c-checkbox:last-child::after { 91 | display: inline-block; 92 | border-radius: 2px; 93 | } 94 | 95 | .c-checkbox :last-child::before { 96 | content: ""; 97 | display: inline-block; 98 | width: 14px; 99 | height: 14px; 100 | margin-right: 5px; 101 | border-radius: 2px; 102 | border: 1px solid #6f6f6f; 103 | transform: translateY(2px); 104 | transition: box-shadow 150ms, background-color 150ms; 105 | } 106 | 107 | .c-checkbox :last-child::after { 108 | font-family: sans-serif; 109 | content: "✓"; 110 | line-height: 1; 111 | font-size: 14px; 112 | position: absolute; 113 | width: 6px; 114 | height: 6px; 115 | top: 7px; 116 | left: 6px; 117 | color: #fff; 118 | font-weight: 600; 119 | transform: translate(-50%, -50%) scale(0); 120 | transition: transform 150ms; 121 | transform-origin: 50% 50%; 122 | } 123 | 124 | .c-checkbox input:checked + :last-child::before { 125 | border-color: #8c00ff; 126 | background-color: #8c00ff; 127 | } 128 | 129 | .c-checkbox input:checked + :last-child::after { 130 | transform: translate(-50%, -50%) scale(1); 131 | } 132 | 133 | /* ------- OTHER STUFF ------- */ 134 | 135 | .toggleArea { 136 | display: flex; 137 | } 138 | 139 | .g-card { 140 | margin-bottom: 0; 141 | } 142 | 143 | .footer { 144 | text-align: center; 145 | padding-top: 0; 146 | } 147 | -------------------------------------------------------------------------------- /src/exercises/2.2.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Exercise 2.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 35 | 36 | 37 | 38 | 39 | 53 |
54 | 55 |
56 |

57 | Hidden elements 58 |

59 | 60 | 61 |
62 | 66 |

Creating custom checkboxes is cool!

67 |
68 | 69 | 70 | 71 |
72 |
73 | 76 | 77 |
78 | Here's a the article. 79 |
80 |
81 |
82 |

A dummy link for the demo.

83 |
84 | 85 | 86 | 87 | 115 | 116 | -------------------------------------------------------------------------------- /src/exercises/2.3.css: -------------------------------------------------------------------------------- 1 | @import url("2.2.css"); 2 | 3 | .skipLink { 4 | /* some predefined visuals */ 5 | color: inherit; 6 | border: 1px solid #9d79ef; 7 | background-color: #fff; 8 | padding: 8px; 9 | /* 💡 Complete the skipLink */ 10 | /* ... */ 11 | } 12 | 13 | /* 💡 Bonus #1 */ 14 | .your-selector-here { 15 | /* Here's a pre-built animation for you */ 16 | animation: ring 1500ms; 17 | } 18 | 19 | /* 💡 Bonus #2 */ 20 | .listTitle:focus { 21 | /* ...custom styles... */ 22 | } 23 | 24 | /* ----------------------------------------- *\ 25 | |* You do not need to touch the code below *| 26 | |* but you can see how it was made. *| 27 | \** ----------------------------------------- */ 28 | 29 | .listBottom { 30 | display: flex; 31 | justify-content: flex-end; 32 | } 33 | 34 | .btnCircle svg { 35 | width: 16px; 36 | fill: currentColor; 37 | } 38 | 39 | @-webkit-keyframes ring { 40 | 30%, 41 | 75% { 42 | box-shadow: white 0 0 0 1px, #ef7979 0 0 0 3px; 43 | } 44 | } 45 | 46 | @keyframes ring { 47 | 30%, 48 | 75% { 49 | box-shadow: white 0 0 0 1px, #ef7979 0 0 0 3px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/exercises/2.3.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | Exercise #2.3 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 33 |
34 | 35 |
36 |

37 | Navigation shortcuts 38 |

39 | 40 |
41 | 45 |

46 | The "skip navigation" idea was invented to give screen reader and keyboard users the 47 | same capability of going directly to the main content, which most users take for granted. 48 |

49 | 53 |
54 | 55 |
56 | 57 | 58 |

A list of things

59 |

60 | Learn more about accessible 62 | scroll-to-top buttons. 63 |

64 |
    65 |
  • Thing 1
  • 66 |
  • Thing 2
  • 67 |
  • Thing 3
  • 68 |
  • Thing 4
  • 69 |
  • Thing 5
  • 70 |
  • Thing 6
  • 71 |
  • Thing 7
  • 72 |
  • Thing 8
  • 73 |
  • Thing 9
  • 74 |
  • Thing 10
  • 75 |
  • Thing 11
  • 76 |
  • Thing 12
  • 77 |
  • Thing 13
  • 78 |
  • Thing 14
  • 79 |
  • Thing 15
  • 80 |
  • Thing 16
  • 81 |
  • Thing 17
  • 82 |
  • Thing 18
  • 83 |
  • Thing 19
  • 84 |
  • Thing 20
  • 85 |
  • Thing 21
  • 86 |
  • Thing 22
  • 87 |
  • Thing 23
  • 88 |
  • Thing 24
  • 89 |
  • Thing 25
  • 90 |
  • Thing 26
  • 91 |
  • Thing 27
  • 92 |
  • Thing 28
  • 93 |
  • Thing 29
  • 94 |
  • Thing 30
  • 95 |
  • Thing 31
  • 96 |
  • Thing 32
  • 97 |
  • Thing 33
  • 98 |
  • Thing 34
  • 99 |
  • Thing 35
  • 100 |
  • Thing 36
  • 101 |
  • Thing 37
  • 102 |
  • Thing 38
  • 103 |
  • Thing 39
  • 104 |
  • Thing 40
  • 105 |
  • Thing 41
  • 106 |
  • Thing 42
  • 107 |
  • Thing 43
  • 108 |
  • Thing 43
  • 109 |
  • Thing 44
  • 110 |
  • Thing 45
  • 111 |
  • Thing 46
  • 112 |
  • Thing 47
  • 113 |
  • Thing 48
  • 114 |
  • Thing 49
  • 115 |
  • Thing 50
  • 116 |
117 |
118 | 126 |
127 | Back to home 128 | 129 | 147 |
148 |
149 |
150 | 151 | 152 | -------------------------------------------------------------------------------- /src/exercises/3.1.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Exercise #3.1 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

Meaningful content

23 | 24 | 25 |
26 |

Case #1

27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 |
35 |

Case #2

36 | 38 | 46 | 60 |
61 | 62 | 63 |
64 |

Case #3

65 |

66 | Links should make sense out of context. Phrases such as "click here" or "read more" 67 | are ambiguous when read out of context. 68 | 69 | 70 | Read more. 71 | 72 |

73 | 74 |

75 | Adding alternative text for images is the first principle of web accessibility. It is also one 76 | of the most difficult to properly implement. 77 | 78 | Read more. 79 | 80 |

81 | 82 |

83 | Please, read more about writing 84 | guidelines here. 85 |

86 |
87 | 88 | 89 |
90 |

Case #4

91 | 96 | 103 |
104 | 105 | 106 | 109 |
110 |

Case #5

111 |
112 | 113 | 114 | 115 | Your subscription will end in 6 days. 116 |
117 |
118 | 119 | 120 |
121 |

Case #6

122 | 124 |

125 | Made with 126 | love ❤️ 127 | instead of ☕, 128 | just for you! 129 |

130 |
131 | 132 | 133 |
134 |

Case #7

135 | 142 |
143 |             __     __
144 |            /  \~~~/  \
145 |      ,----(     ..    )
146 |     /      \__     __/
147 |    /|         (\  |(
148 |   ^ \   /___\  /\ | ` 
149 |     |__|    |__| -"   
150 |         
151 |

ASCII art is cool but meaningless for screen readers.

152 |
153 |
154 | 155 | 156 | -------------------------------------------------------------------------------- /src/exercises/3.2.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Exercise #3.2 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 36 | 37 |
38 |

Page Landmarks

39 |
40 |

Good morning across the world

41 |

42 | In a world of around 6,000 languages, there are tons of ways to greet each other and say good morning. 43 | Here are three examples: 44 |

45 | 46 |
    47 |
  • 48 | 49 | Bonjour - French 50 |
  • 51 |
  • 52 | 53 | Bom dia - Portuguese (Portugal) 54 |
  • 55 |
  • 56 | 57 | Günaydin - Turkish 58 |
  • 59 |
60 | 61 | Learn to say "Good morning" in 62 | many languages. 63 |
64 |
65 | 66 | 67 |
68 | Nobody left a comment yet... 69 |
70 | 71 | 81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /src/exercises/3.3-short.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | Exercise #3.3 (short) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |

Accessible Forms

26 | 27 | 28 |

Form: Web lover profile

29 | 30 | 37 |
38 | 41 | 42 | 43 | 44 | Your alternative name to be used in the web world 45 | 46 |
47 | 48 | 52 |
53 |

Account

54 | 58 | 59 | 60 | 63 |
64 | 65 | 66 | 67 | 71 | 72 |
73 | 74 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/exercises/4.1.css: -------------------------------------------------------------------------------- 1 | /* 💡 BONUS 1/2 - You may need to change this selector */ 2 | .btnSubmit:hover:not(:disabled) { 3 | opacity: 0.8; 4 | } 5 | 6 | /* 💡 BONUS 2/2 - You may need to change this selector */ 7 | .btnSubmit:disabled { 8 | opacity: 0.7; 9 | cursor: not-allowed; 10 | } 11 | 12 | /* ----------------------------------------- *\ 13 | |* You do not need to touch the code below *| 14 | |* but you can see how it was made. *| 15 | \** ----------------------------------------- */ 16 | 17 | .js-isHidden { 18 | display: none; 19 | } 20 | 21 | .area { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | padding-bottom: 24px; 26 | } 27 | 28 | .areaEnd { 29 | position: relative; 30 | text-align: right; 31 | } 32 | 33 | .field { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | .fieldLabel { 39 | font-size: 1.8rem; 40 | font-weight: 600; 41 | margin-right: 4px; 42 | } 43 | 44 | .fieldInput { 45 | width: 50px; 46 | height: 40px; 47 | border: none; 48 | border: 1px solid var(--theme-text_1); 49 | border-radius: 4px; 50 | text-align: center; 51 | font-size: 1.8rem; 52 | } 53 | .fieldInput:focus { 54 | box-shadow: var(--theme-focus_shadow); 55 | outline: var(--theme-focus_outline); 56 | } 57 | .fieldInput::-webkit-outer-spin-button, 58 | .fieldInput::-webkit-inner-spin-button { 59 | -webkit-appearance: none; 60 | margin: 0; 61 | } 62 | .fieldInput[type="number"] { 63 | -moz-appearance: textfield; 64 | } 65 | 66 | .btnSubmit { 67 | --btnTxt: #fff; 68 | position: relative; 69 | display: inline-block; 70 | cursor: pointer; 71 | min-height: 44px; 72 | padding: 8px 20px; 73 | background-color: var(--theme-primary); 74 | border-radius: 4px; 75 | border: none; 76 | font-size: 1.8rem; 77 | color: var(--btnTxt); 78 | text-align: center; 79 | transition: color 250ms; 80 | } 81 | 82 | /* ... disabled styles moved to the top ... */ 83 | 84 | .btnSubmit:focus:not(:focus-visible) { 85 | outline: none; 86 | } 87 | 88 | .btnSubmit:focus-visible { 89 | box-shadow: var(--theme-focus_shadow); 90 | outline: var(--theme-focus_outline); 91 | } 92 | 93 | /* loading indicator */ 94 | .btnSubmit::after { 95 | content: ""; 96 | position: absolute; 97 | display: block; 98 | width: 0.7em; 99 | height: 0.7em; 100 | top: calc(50% - 0.5em); 101 | left: calc(50% - 0.5em); 102 | border: 2px var(--btnTxt); 103 | border-bottom-color: transparent; 104 | border-left-color: transparent; 105 | border-style: solid; 106 | border-radius: 50%; 107 | opacity: 0; 108 | transition: opacity 250ms; 109 | } 110 | 111 | .btnSubmit[data-loading="true"] { 112 | color: transparent; 113 | pointer-events: none; 114 | } 115 | .btnSubmit[data-loading="true"]::after { 116 | opacity: 1; 117 | animation: rotate 750ms linear infinite; 118 | } 119 | 120 | .btnSubmit[data-loading="true"] .btnSubmit-text { 121 | visibility: hidden; 122 | } 123 | 124 | .formStatus { 125 | position: absolute; 126 | top: 100%; 127 | right: 0; 128 | font-size: 1.3rem; 129 | color: green; 130 | margin-top: 6px; 131 | } 132 | 133 | .terms { 134 | position: relative; 135 | margin: 16px 0 0; 136 | text-align: center; 137 | } 138 | 139 | .tooltipArea { 140 | position: relative; 141 | } 142 | 143 | .tooltipBox { 144 | position: absolute; 145 | width: 104px; 146 | bottom: 100%; 147 | left: calc(50% - 52px); 148 | padding-bottom: 4px; /* use padding to preserve hover when moving cursor between the tooltip button and the tooltipItself */ 149 | 150 | opacity: 0; 151 | visibility: hidden; 152 | 153 | /* delay 250ms to give time to fade out */ 154 | transition: opacity 250ms, visibility 1ms 250ms; 155 | } 156 | 157 | .tooltipArea.isActive:hover .tooltipBox, 158 | .tooltipArea.isActive:focus-within .tooltipBox { 159 | opacity: 1; 160 | visibility: visible; 161 | transition: opacity 250ms; 162 | } 163 | .tooltipArea.isActive:hover .tooltipBox { 164 | /* delay fadein 500ms to prevent accidental hovers */ 165 | transition: opacity 250ms 500ms; 166 | } 167 | 168 | .tooltipItself { 169 | display: block; 170 | background: hsl(266deg 100% 15%); 171 | color: hsl(266deg 100% 96%); 172 | line-height: 1.2; 173 | padding: 6px 8px; 174 | font-size: 1.3rem; 175 | border-radius: 4px; 176 | text-align: center; 177 | -webkit-font-smoothing: initial; 178 | -moz-osx-font-smoothing: initial; 179 | } 180 | 181 | @keyframes rotate { 182 | 0% { 183 | transform: rotate(0deg); 184 | } 185 | 100% { 186 | transform: rotate(360deg); 187 | } 188 | } 189 | 190 | .resultsArea { 191 | width: 100%; 192 | min-height: 100px; 193 | border: 1px dotted var(--theme-primary); 194 | margin-top: 10px; 195 | padding: 10px; 196 | display: flex; 197 | align-items: center; 198 | justify-content: center; 199 | } 200 | -------------------------------------------------------------------------------- /src/exercises/4.1.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Exercise #4.1 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

Loading states

20 | 21 |
22 |
23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 |
31 |
32 | 35 | 36 | 39 |
40 |
41 |
42 | 43 |
44 |

No products yet.

45 | 46 |

47 | 48 |

49 | 50 |
51 | 52 |
53 |
54 | 55 |

A dummy link for demo.

56 |
57 | 58 |
59 | 60 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/exercises/4.1.react.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Exercise #4.1 (React) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

A modern browser is needed to run this exercise with React.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/exercises/4.1.react.js: -------------------------------------------------------------------------------- 1 | /* 2 | 🍀 Some things to know about this: 3 | - Everything is fine with the form. Focus on the results area. 4 | - You can change the loading time at line 130. 5 | - The page "7" will throw a results error. 6 | */ 7 | 8 | function Exercise() { 9 | const [page, setPage] = React.useState(""); // input page value 10 | const [products, setProcuts] = React.useState([]); 11 | const [isLoadingProducts, setIsLoadingProducts] = React.useState(false); 12 | const [productsError, setProductsError] = React.useState(null); 13 | const isFormValid = page >= 1 && page <= 9; 14 | const isProductsOkay = !productsError && !isLoadingProducts; 15 | const isFormSubmitActive = isFormValid && !isLoadingProducts; 16 | 17 | async function handleSubmit(e) { 18 | e.preventDefault(); // avoid native form submit (page refresh) 19 | 20 | setIsLoadingProducts(true); 21 | setProductsError(null); 22 | 23 | try { 24 | const productsList = await fetchProducts(page); 25 | setProcuts(productsList); 26 | } catch (err) { 27 | const errorMessage = `Ups, something went wrong.`; 28 | setProductsError(errorMessage); 29 | } finally { 30 | setIsLoadingProducts(false); 31 | } 32 | } 33 | 34 | return ( 35 |
36 |

Loading states

37 | 38 |
39 | {/* 🍀 Everything is okay with the form itself. 40 | For this exercise, focus just on the results area :) */} 41 |
42 |
43 |
44 | 47 | setPage(Number(e.target.value))} 57 | /> 58 |
59 |
60 | 61 |
62 |
63 | 72 | 73 | 76 |
77 |
78 |
79 | 80 |
81 | {/* Empty State */} 82 | {products.length === 0 && isProductsOkay &&

No products yet.

} 83 | 84 | {/* 💡 Below it's the multiple dynamic states. 85 | Create the necessary live regions to ensure the 86 | dynamic content is announced by assistive technologies. */} 87 | 88 | {/* Loading state */} 89 | {isLoadingProducts &&

Loading products...

} 90 | 91 | {/* Error state */} 92 | {productsError &&

{productsError}

} 93 | 94 | {/* Products list */} 95 | {products.length > 0 && isProductsOkay && ( 96 |
97 |
    98 | {products.map((product) => ( 99 |
  • Product {product.id}
  • 100 | ))} 101 |
102 |
103 | )} 104 |
105 | 106 |

107 | A{" "} 108 | 109 | dummy link 110 | {" "} 111 | for demo. 112 |

113 |
114 |
115 | ); 116 | } 117 | 118 | /* ============================== *\ 119 | \* You can ignore the code below. */ 120 | 121 | async function fetchProducts(page) { 122 | console.log("Loading products from page:", page); 123 | 124 | if (page === 7) { 125 | // Simulate a problem with this page for demo purposes. 126 | await fakeWaitTime(500); 127 | throw Error("Page 7 is unstable."); 128 | } 129 | 130 | await fakeWaitTime(2500); 131 | 132 | // Return a dummy array of 10 items. Each item is an object. eg 133 | // [{ id: 20 }, { id: 21 }, { id: 22 }, { id: 23 }, ...] 134 | return Array.from(Array(10), (_, i) => ({ 135 | id: `${page}${i}`, 136 | })); 137 | } 138 | 139 | function fakeWaitTime(ms) { 140 | return new Promise((resolve) => setTimeout(resolve, ms)); 141 | } 142 | 143 | const rootElement = document.getElementById("root"); 144 | ReactDOM.render(, rootElement); 145 | -------------------------------------------------------------------------------- /src/exercises/4.2.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- / 2 | 💡 1/2 The only CSS you might need to change 3 | is at the bottom of the this file 💡 4 | --------------------------------------- */ 5 | 6 | /* Control appearance based on data-* */ 7 | .stgs[data-theme="light"] { 8 | background: var(--theme-bg_1); 9 | color: var(--theme-text_0); 10 | } 11 | .stgs[data-theme="dark"] { 12 | background: var(--theme-text_0); 13 | color: var(--theme-bg_0); 14 | } 15 | 16 | .stgs[data-size="sm"] { 17 | font-size: 1.4rem; 18 | } 19 | .stgs[data-size="md"] { 20 | font-size: 1.6rem; 21 | } 22 | .stgs[data-size="lg"] { 23 | font-size: 1.9rem; 24 | } 25 | 26 | /* ---- Theme toggle ---- */ 27 | .stgs-toggleTheme { 28 | background: transparent; 29 | color: inherit; 30 | padding: 12px 16px; 31 | border: none; 32 | border-radius: 4px; 33 | border: 1px solid; 34 | } 35 | .stgs-toggleTheme:focus-within { 36 | box-shadow: var(--theme-focus_shadow); 37 | outline: var(--theme-focus_outline); 38 | } 39 | .stgs-toggleTheme input { 40 | position: absolute; 41 | opacity: 0; 42 | } 43 | .stgs-toggleTheme input + .stgs-toggleTheme-fakeCheck::before { 44 | content: "🔲"; 45 | content: "🔲" / ""; 46 | margin-right: 8px; 47 | } 48 | .stgs-toggleTheme input:checked + .stgs-toggleTheme-fakeCheck::before { 49 | content: "✅"; 50 | content: "✅" / ""; 51 | margin-right: 8px; 52 | } 53 | 54 | /* ---- Sound toggle ---- */ 55 | .stgs-btnOnOff { 56 | background: transparent; 57 | color: inherit; 58 | padding: 12px 16px; 59 | border: none; 60 | border-radius: 4px; 61 | border: 1px solid; 62 | } 63 | .stgs-btnOnOff:focus { 64 | box-shadow: var(--theme-focus_shadow); 65 | outline: var(--theme-focus_outline); 66 | } 67 | .stgs-btnOnOff .on, 68 | .stgs-btnOnOff .off { 69 | display: none; 70 | } 71 | .stgs-btnOnOff[aria-pressed="true"] .on { 72 | display: block; 73 | } 74 | .stgs-btnOnOff[aria-pressed="false"] .off { 75 | display: block; 76 | } 77 | 78 | /* ---- Font size options ---- */ 79 | .stgs-toolbar { 80 | display: flex; 81 | flex-wrap: wrap; 82 | border: 0; 83 | padding: 0; 84 | } 85 | 86 | .stgs-toolbar-title { 87 | display: block; 88 | width: 100%; 89 | } 90 | .stgs-toolbar-item { 91 | border: none; 92 | border: 1px solid var(--theme-text_1); 93 | padding: 10px 10px; 94 | margin: 0; 95 | min-height: 44px; 96 | font-weight: 600; 97 | background: transparent; 98 | color: inherit; 99 | } 100 | 101 | /** 💡 2/2 Probably only these 4 selectors 102 | need to be changed to keep the same styles */ 103 | .stgs-toolbar input { 104 | opacity: 0; 105 | position: absolute; 106 | } 107 | 108 | .stgs-toolbar input:focus + .stgs-toolbar-item { 109 | box-shadow: var(--theme-focus_shadow); 110 | outline: var(--theme-focus_outline); 111 | } 112 | 113 | .stgs-toolbar input:checked + .stgs-toolbar-item { 114 | background-color: var(--theme-primary); 115 | color: var(--theme-bg_1); 116 | } 117 | 118 | .stgs-toolbar input:disabled + .stgs-toolbar-item { 119 | opacity: 0.6; 120 | cursor: not-allowed; 121 | } 122 | -------------------------------------------------------------------------------- /src/exercises/4.2.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Exercise #4.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 |
34 |

On-demand changes

35 | 36 |
37 | 38 | 39 |
40 | 45 | 50 |
51 | 52 | 53 |
54 | 59 | 67 |
68 | 69 | 70 |
71 | 78 |
79 | Text size 80 | 81 | 82 | 85 | 86 | 87 | 90 | 91 | 92 | 95 | 96 | 97 | 100 |
101 | 102 |

Check a Toolbar 103 | example, 104 | following WAI ARIA practices.

105 |
106 |
107 |
108 | 109 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/exercises/9_arhive.css: -------------------------------------------------------------------------------- 1 | /* --------------------------------------- */ 2 | /* -------- CASE #0 CARDS CLICKABLE ------ */ 3 | /* --------------------------------------- */ 4 | 5 | .cardParent { 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | .card { 11 | display: grid; 12 | grid-template-columns: 50% 50%; 13 | grid-template-rows: auto auto auto auto 1fr; 14 | grid-template-areas: 15 | "placeholder placeholder" 16 | "tag date" 17 | "title title" 18 | "text text" 19 | "cta cta"; 20 | width: calc(50% - 8px); 21 | border: 1px solid gray; 22 | border-radius: 1px; 23 | padding: 8px; 24 | } 25 | 26 | .card-link { 27 | color: inherit; 28 | text-decoration: none; 29 | } 30 | 31 | .card:hover { 32 | border-color: var(--theme-primary); 33 | cursor: pointer; 34 | } 35 | 36 | .card:hover .card-link { 37 | color: var(--theme-primary); 38 | } 39 | 40 | .card-link:focus { 41 | outline: none; 42 | color: var(--theme-primary); 43 | } 44 | 45 | .card-link:focus .card-decoration, 46 | .card-link:hover .card-decoration { 47 | background: var(--theme-primary); 48 | } 49 | 50 | .card-link:focus .card-title, 51 | .card-link:hover .card-title { 52 | color: var(--theme-primary); 53 | } 54 | 55 | .card-decoration { 56 | grid-area: placeholder; 57 | height: 50px; 58 | width: calc(100% + 16px); 59 | margin: -8px 0 0 -8px; 60 | background: var(--theme-primary_smooth); 61 | } 62 | 63 | .card-tag { 64 | grid-area: tag; 65 | align-self: center; 66 | color: gray; 67 | padding-top: 16px; 68 | text-transform: uppercase; 69 | font-size: 1.2rem; 70 | letter-spacing: 0.1em; 71 | font-weight: 600; 72 | } 73 | 74 | .card-date { 75 | grid-area: date; 76 | align-self: center; 77 | justify-self: end; 78 | color: gray; 79 | padding-top: 16px; 80 | font-size: 1.2rem; 81 | } 82 | 83 | .card-title { 84 | grid-area: title; 85 | margin: 0; 86 | padding: 4px 0 8px; 87 | line-height: 1.2; 88 | } 89 | 90 | .card-text { 91 | grid-area: text; 92 | margin: 0; 93 | } 94 | 95 | .card-cta { 96 | grid-area: cta; 97 | color: inherit; 98 | text-decoration: none; 99 | color: var(--theme-primary); 100 | text-decoration: underline; 101 | align-self: end; 102 | padding-top: 8px; 103 | } 104 | 105 | /* --------------------------------------- */ 106 | /* --------- CASE #0 CUSTOM SELECT ------- */ 107 | /* --------------------------------------- */ 108 | 109 | /* Both native and custom selects must have the same width/height. */ 110 | .selectNative, 111 | .selectCustom { 112 | position: relative; 113 | width: 220px; 114 | height: 40px; 115 | } 116 | 117 | .selectCustom { 118 | /* 💡 The custom select must be absolute to not mess with the layout */ 119 | /* position: absolute; */ 120 | top: 0; 121 | left: 0; 122 | } 123 | 124 | /* 💡 Write the select styles from here... */ 125 | 126 | /* 1/4 hide the custom select by default */ 127 | 128 | /* 2/4 within the hover media query 129 | - 3/4 show the custom select 130 | - 4/4 hiden the custom select when the native is focused 131 | */ 132 | 133 | /* ... until here 💡 */ 134 | 135 | /* Once you make the native select work, that's all! */ 136 | 137 | /* ----- */ 138 | 139 | /* ----------------------------------------- *\ 140 | |* You do not need to touch the code below *| 141 | |* but you can see how it was made. *| 142 | \** ----------------------------------------- */ 143 | 144 | /* highlight the custom select when is clicked */ 145 | .selectCustom.isActive .selectCustom-trigger { 146 | box-shadow: var(--theme-focus_shadow); 147 | outline: var(--theme-focus_outline); 148 | } 149 | 150 | /* Rest of the styles to create the custom select. */ 151 | /* Just make sure the native and the custom have a similar "box" (the trigger). */ 152 | 153 | .select { 154 | position: relative; 155 | } 156 | 157 | .selectLabel { 158 | display: block; 159 | font-weight: bold; 160 | margin-bottom: 4px; 161 | } 162 | 163 | .selectWrapper { 164 | position: relative; 165 | } 166 | 167 | .selectNative, 168 | .selectCustom-trigger { 169 | font-size: 1.6rem; 170 | background-color: #fff; 171 | border: 1px solid #6f6f6f; 172 | border-radius: 4px; 173 | } 174 | 175 | .selectNative { 176 | -webkit-appearance: none; 177 | -moz-appearance: none; 178 | background-image: url("data:image/svg+xml;utf8,"); 179 | background-repeat: no-repeat; 180 | background-position-x: 100%; 181 | background-position-y: 8px; 182 | padding: 0 8px; 183 | } 184 | 185 | .selectCustom-trigger { 186 | position: relative; 187 | width: 100%; 188 | height: 100%; 189 | background-color: #fff; 190 | padding: 8px 8px; 191 | cursor: pointer; 192 | } 193 | 194 | .selectCustom-trigger::after { 195 | content: "▾"; 196 | position: absolute; 197 | top: 0; 198 | line-height: 38px; 199 | right: 8px; 200 | } 201 | 202 | .selectCustom-trigger:hover { 203 | border-color: #8c00ff; 204 | } 205 | 206 | .selectCustom-options { 207 | position: absolute; 208 | top: calc(38px + 8px); 209 | left: 0; 210 | width: 100%; 211 | border: 1px solid #6f6f6f; 212 | border-radius: 4px; 213 | background-color: #fff; 214 | box-shadow: 0 0 4px #e9e1f8; 215 | z-index: 1; 216 | padding: 8px 0; 217 | display: none; 218 | } 219 | 220 | .selectCustom.isActive .selectCustom-options { 221 | display: block; 222 | } 223 | 224 | .selectCustom-option { 225 | position: relative; 226 | padding: 8px; 227 | padding-left: 25px; 228 | } 229 | 230 | .selectCustom-option.isHover, 231 | .selectCustom-option:hover { 232 | background-color: #865bd7; /* contrast AA */ 233 | color: white; 234 | cursor: default; 235 | } 236 | 237 | .selectCustom-option:not(:last-of-type)::after { 238 | content: ""; 239 | position: absolute; 240 | bottom: 0; 241 | left: 0; 242 | width: 100%; 243 | border-bottom: 1px solid #d3d3d3; 244 | } 245 | 246 | .selectCustom-option.isActive::before { 247 | content: "✓"; 248 | position: absolute; 249 | left: 8px; 250 | } 251 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/favicon.ico -------------------------------------------------------------------------------- /src/solutions/1.1.css: -------------------------------------------------------------------------------- 1 | /* ------- Solving Bonus 1: 2 | - 💡Your solution may require to change .header or .header-tag 3 | */ 4 | .header { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: flex-start; 8 | } 9 | 10 | .header-tag { 11 | order: -1; 12 | margin: 0; 13 | } 14 | 15 | /* ------- Solving Bonus 2: ------- */ 16 | /* Note: This is the same as .sr-only during the next exercises */ 17 | .visually-hidden { 18 | position: absolute; 19 | width: 1px; 20 | height: 1px; 21 | padding: 0; 22 | margin: -1px; 23 | overflow: hidden; 24 | clip: rect(0, 0, 0, 0); /* old browsers */ 25 | clip-path: inset(50%); /* modern browsers */ 26 | border: 0; 27 | } 28 | 29 | /* ----------------------------------------- *\ 30 | |* You do not need to touch the code below *| 31 | |* but you can see how it was made. *| 32 | \** ----------------------------------------- */ 33 | 34 | .title-xl { 35 | font-size: 3.2rem; 36 | font-weight: 600; 37 | margin: 0 0 16px; 38 | line-height: 1.2; 39 | } 40 | 41 | .title-lg { 42 | font-size: 2.4rem; 43 | font-weight: 600; 44 | margin: 0 0 16px; 45 | line-height: 1.2; 46 | } 47 | 48 | .title-md { 49 | font-size: 1.8rem; 50 | font-weight: 600; 51 | margin: 0 0 16px; 52 | line-height: 1.2; 53 | } 54 | 55 | .text-sm { 56 | font-size: 1.4rem; 57 | } 58 | 59 | /* ------- Header ------- */ 60 | 61 | .company-links { 62 | display: flex; 63 | justify-content: center; 64 | padding: 32px 0 16px; 65 | margin: 0 0 -90px; 66 | } 67 | 68 | .company-links { 69 | list-style: none; 70 | padding-right: 0; 71 | } 72 | 73 | .company-links a { 74 | margin: 0 8px; 75 | } 76 | 77 | /* ------- Main ------- */ 78 | 79 | .tagSecondary { 80 | padding: 2px 4px; 81 | text-transform: uppercase; 82 | font-size: 1.4rem; 83 | font-weight: 600; 84 | background-color: #ffe6d2; 85 | color: #a75000; 86 | line-height: 1; 87 | } 88 | 89 | .header-plantName { 90 | margin-bottom: 16px; 91 | } 92 | 93 | /* ------- Info ------- */ 94 | 95 | .info { 96 | display: flex; 97 | justify-content: space-between; 98 | align-items: center; 99 | margin: 16px 0; 100 | } 101 | 102 | .info-part { 103 | flex: 1; 104 | } 105 | 106 | .topic { 107 | margin: 0; 108 | padding: 0; 109 | } 110 | 111 | .topic-item { 112 | display: flex; 113 | flex-direction: column; 114 | margin: 0 0 16px; 115 | padding: 0; 116 | } 117 | 118 | .topic-key { 119 | font-size: 1.4rem; 120 | color: #595959; 121 | } 122 | 123 | .topic-value { 124 | margin: 4px 0; 125 | } 126 | 127 | .media { 128 | text-align: center; 129 | } 130 | 131 | .media-img { 132 | max-width: 100%; 133 | } 134 | 135 | /* ------- Details & CTA ------- */ 136 | 137 | .details { 138 | padding-top: 16px; 139 | margin-top: 16px; 140 | border-top: 1px solid var(--theme-primary); 141 | } 142 | 143 | .details-part:nth-child(2) { 144 | display: flex; 145 | justify-content: space-between; 146 | align-items: center; 147 | margin-top: 16px; 148 | } 149 | 150 | .details-text { 151 | line-height: 1.5; 152 | font-size: 1.6rem; 153 | margin: 0; 154 | } 155 | 156 | .price { 157 | line-height: 1; 158 | } 159 | 160 | .price-original { 161 | text-decoration: line-through; 162 | opacity: 0.7; 163 | margin: 0 0 4px; 164 | } 165 | 166 | .price-final { 167 | margin-bottom: 0; 168 | } 169 | -------------------------------------------------------------------------------- /src/solutions/1.1.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Solution #1.1 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 40 |
41 |
42 |
43 |
44 | 46 |

47 | Monstera Deliciosa 48 |

49 |

On Sale

50 |
51 | 52 |
53 |

Characteristics

54 | 56 |
    57 |
  • 58 | Family 59 | Araceae 60 |
  • 61 |
  • 62 | Temperature 63 | 20ºC 64 |
  • 65 |
  • 66 | Water 67 | 1/week 68 |
  • 69 |
70 | 71 |
72 | a plant in a vase 73 |
74 |
75 | 76 |
77 |
78 |

Details

79 |

Monstera deliciosa, also known as the Swiss cheese plant, is a species of flowering 80 | plant native to tropical forests of southern Mexico, south to Panama.

81 |
82 | 83 | 84 |
85 |
86 | 88 |

89 | 90 | Old price: $40.00 91 |

92 |

93 | 94 | New price: $30.00 95 | 96 |

97 | 99 |
100 | 101 |
102 |
103 |
104 |
105 |
106 | 107 | 108 | 109 | 115 | 116 | -------------------------------------------------------------------------------- /src/solutions/1.2.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | Solution #1.2 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 |
25 |
26 | Breaking news 27 |

Fat cats love you

28 |
29 |
30 |

31 | Cat ipsum. Instead of drinking water from the cat bowl, make sure to steal water from the 32 | toilet sleeps. 33 | Stretch out on bed find empty spot in cupboard and sleep all day. 34 |

35 |
36 | 37 |

Checklist to get a cat:

38 | 39 |
    40 |
  • 41 | Food and water bowls 42 |
  • 43 |
  • 44 | Cat bed to sleep 45 |
  • 46 |
  • 47 | Toys and time to play 48 |
  • 49 |
  • 50 | Happy home 51 |
  • 52 |
53 |
54 | 55 |

Did you know? Cats sleep 12-16 hours per day.

56 | 57 | 65 |
66 | 67 |
68 | 74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/solutions/1.3.css: -------------------------------------------------------------------------------- 1 | /* ---------------------------------------- *\ 2 | |* You do not need to touch this file *| 3 | |* but you can see how it was made. *| 4 | \** ---------------------------------------- */ 5 | 6 | /* Case 1 */ 7 | .case1 { 8 | position: relative; 9 | width: 100%; 10 | background: black; 11 | padding: 0; 12 | } 13 | 14 | .case1-img { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | object-fit: cover; 21 | } 22 | 23 | .case1-text { 24 | position: relative; 25 | margin: 0; 26 | padding: 40px 24px; 27 | font-size: 3.2rem; 28 | color: white; 29 | text-shadow: 1px 1px 3px #00000080; 30 | } 31 | 32 | /* Case 2 */ 33 | .case2 { 34 | display: flex; 35 | } 36 | 37 | .case2 img { 38 | width: 48px; 39 | height: 48px; 40 | border-radius: 50%; 41 | } 42 | 43 | .case2-content { 44 | margin-left: 16px; 45 | } 46 | 47 | .case2-name { 48 | display: block; 49 | font-weight: 600; 50 | margin-bottom: 4px; 51 | } 52 | 53 | .case2-quote { 54 | font-style: italic; 55 | } 56 | 57 | .case2-quote::before, 58 | .case2-quote::after { 59 | content: ""; 60 | } 61 | 62 | /* Case 3 */ 63 | .case3 { 64 | max-width: 250px; 65 | border: 1px solid #6f6f6f; 66 | padding: 16px; 67 | } 68 | 69 | .case3 figure { 70 | margin: 0; 71 | } 72 | 73 | .case3-img { 74 | display: block; 75 | width: 100%; 76 | aspect-ratio: 1.25; 77 | background: #2b260f; /* mozart bg color */ 78 | color: white; 79 | padding: 16px; 80 | } 81 | 82 | .case3-caption { 83 | font-style: italic; 84 | font-size: 1.4rem; 85 | margin-top: 4px; 86 | } 87 | 88 | /* Case 4 */ 89 | .case4 { 90 | display: flex; 91 | align-items: center; 92 | } 93 | 94 | .case4-text { 95 | margin-left: 4px; 96 | } 97 | -------------------------------------------------------------------------------- /src/solutions/1.3.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Solution #1.3 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |

Meaningful images

26 | 27 | 28 |
29 | 30 | 31 |

Start your piano classes today!

32 |
33 | 34 | 35 |
36 | 38 | 39 |
40 | Wolfgang Amadeus Mozart 41 | 42 | The music is not in the notes, but in the silence between. 43 | 44 |
45 |
46 | 47 | 48 | 49 |
50 | 52 |
53 | Mozart standing in front of his piano 54 |
55 | Anonymous portrait of the child Mozart, possibly by Pietro Antonio Lorenzoni; painted in 1763. 56 |
57 |
58 |
59 | 60 | 61 | 62 | 66 | Soundcloud 67 | Classical music playlist 68 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /src/solutions/2.1.css: -------------------------------------------------------------------------------- 1 | /* Do never ever do this 🥺. 2 | /* *:focus { 3 | outline: none; 4 | } */ 5 | 6 | /* ------- BUTTON ------- */ 7 | 8 | .c-btn { 9 | display: inline-flex; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: 1.8rem; 13 | border: none; 14 | padding: 8px 24px; 15 | background-color: var(--theme-primary); 16 | color: #fff; 17 | border: none; 18 | cursor: pointer; 19 | border-radius: 4px; 20 | /* 💡 Bonus #2 Ensure WCAG 2.5.5 */ 21 | min-height: 44px; 22 | min-width: 44px; 23 | } 24 | 25 | .c-btnPrimary:hover { 26 | opacity: 0.8; 27 | } 28 | 29 | /* 💡 Link focus styles + Bonus #1.1 */ 30 | .c-btnPrimary:focus:not(:focus-visible) { 31 | outline: none; 32 | } 33 | 34 | .c-btnPrimary:focus-visible { 35 | box-shadow: var(--theme-focus_shadow); 36 | outline: var(--theme-focus_outline); 37 | } 38 | 39 | /* 💡 Link focus styles + Bonus #1.2 */ 40 | /* --- 41 | Note: For demo purposes, you'll need to update 42 | the HTML class from c-btnPrimary to c-btnPrimaryJS 43 | --- */ 44 | .js-focus-visible .c-btnPrimaryJS:focus { 45 | outline: none; 46 | } 47 | 48 | .c-btnPrimaryJS:focus.focus-visible { 49 | box-shadow: var(--theme-focus_shadow); 50 | outline: var(--theme-focus_outline); 51 | } 52 | 53 | /* ------- LINK ------- */ 54 | 55 | .c-link { 56 | color: var(--theme-text_0); 57 | padding: 1px 2px; 58 | } 59 | 60 | .c-link:hover { 61 | color: var(--theme-primary); 62 | } 63 | 64 | /* 💡 Link focus styles */ 65 | .c-link:focus { 66 | color: var(--theme-primary); 67 | box-shadow: var(--theme-focus_shadow); 68 | outline: var(--theme-focus_outline); 69 | } 70 | 71 | /* ------- FORM INPUTS ------- */ 72 | 73 | .fieldArea { 74 | max-width: 200px; 75 | } 76 | 77 | .fieldLabel { 78 | display: block; 79 | font-weight: 600; 80 | /* 💡 Bonus #3 Use padding instead of margin to 81 | be included in the interactive area */ 82 | padding-bottom: 8px; 83 | } 84 | 85 | .textInput { 86 | width: 100%; 87 | height: 32px; 88 | padding: 4px 8px; 89 | border-radius: 4px; 90 | border: 1px solid var(--theme-text_1); 91 | } 92 | 93 | /* 💡 Input focus styles */ 94 | 95 | .textInput:hover { 96 | border-color: var(--theme-primary); 97 | } 98 | 99 | .textInput:focus { 100 | box-shadow: var(--theme-focus_shadow); 101 | outline: var(--theme-focus_outline); 102 | } 103 | 104 | /* 💡 Bonus #3 */ 105 | .fieldArea:focus-within .fieldLabel { 106 | color: var(--theme-primary); 107 | } 108 | 109 | /* ------- BUTTON CLOSE ------- */ 110 | 111 | .c-btnCorner { 112 | all: unset; 113 | position: absolute; 114 | --cornerOffset: 16px; 115 | bottom: var(--cornerOffset); 116 | right: var(--cornerOffset); 117 | } 118 | 119 | .c-btnCorner-area { 120 | position: relative; 121 | /* 💡 Bonus #2 - Ensure at least 24x24 122 | to comply with WCAG 2.5.8 (Draft WCAG 2.2) */ 123 | width: 24px; 124 | height: 24px; 125 | border-radius: 50%; 126 | color: var(--theme-text_0); 127 | border: 1px solid; 128 | display: flex; 129 | align-items: center; 130 | justify-content: center; 131 | line-height: 1; 132 | cursor: pointer; 133 | } 134 | 135 | /* 🍀 The hover indicator has enough contrast, 136 | so the focus can be exactly the same */ 137 | .c-btnCorner-area:hover, 138 | .c-btnCorner:focus .c-btnCorner-area { 139 | background-color: var(--theme-primary); 140 | color: var(--theme-bg_0); 141 | transition: background-color 150ms, color 150ms; 142 | } 143 | 144 | .c-btnCorner svg { 145 | width: 9px; 146 | margin-left: 1px; 147 | fill: currentColor; 148 | } 149 | 150 | /* 💡🍀 Bonus #2 - Extra trick to to be applied to "corner buttons" 151 | Use a pseudo selector to increase the interactive area (44x44), 152 | even if visually the button looks smaller (24x24px). */ 153 | .c-btnCorner::before { 154 | content: ""; 155 | position: absolute; 156 | width: 44px; 157 | height: 44px; 158 | bottom: calc(var(--cornerOffset) * -1); 159 | right: calc(var(--cornerOffset) * -1); 160 | } 161 | -------------------------------------------------------------------------------- /src/solutions/2.1.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | Solution #2.1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |

Interactive elements

23 | 24 |
25 | 26 |
27 | 30 |
31 | 32 | 33 |
34 |

35 | 36 | If you are lost, you can go back Home and start over. 37 |

38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/solutions/2.2.css: -------------------------------------------------------------------------------- 1 | /* ------- CASE #1 Menu sidebar ------- */ 2 | 3 | /* No CSS needed to be changed */ 4 | 5 | /* ------- CASE #2 Toggle content ------- */ 6 | 7 | .coolText { 8 | /* 💡 With visibility hidden, it's completely hidden from everyone. */ 9 | visibility: hidden; 10 | padding: 16px 0 0 16px; 11 | 12 | /* 🍀 Animation tip: You can delay the visibility transition 13 | to give time for the opacity animation to finish! */ 14 | opacity: 0; 15 | transition: opacity 250ms, visibility 1ms 250ms; 16 | } 17 | 18 | .coolText.isVisible { 19 | visibility: visible; 20 | 21 | opacity: 1; 22 | transition: opacity 250ms; 23 | } 24 | 25 | /* ------- CASE #3 Custom input ------- */ 26 | 27 | .c-checkbox { 28 | position: relative; 29 | display: inline-block; 30 | cursor: pointer; 31 | margin-right: 15px; 32 | font-size: 1.4rem; 33 | } 34 | 35 | .c-checkbox input { 36 | /* 💡 Write your solution here! 37 | We shouldn't use display:none, right? 38 | /* display: none; */ 39 | /* 🍀 The solution is on the HTML */ 40 | outline: none; 41 | } 42 | 43 | /* ----------------------------------------- *\ 44 | |* You do not need to touch the code below *| 45 | |* but you can see how it was made. *| 46 | \** ----------------------------------------- */ 47 | 48 | /* ------- HEADER + MENU ------- */ 49 | 50 | .headerFixed { 51 | position: fixed; 52 | width: 100%; 53 | display: flex; 54 | justify-content: flex-end; 55 | padding: 8px; 56 | z-index: 2; 57 | } 58 | 59 | .menu { 60 | position: fixed; 61 | top: 0; 62 | right: 0; 63 | z-index: -1; 64 | width: 150px; 65 | height: 100%; 66 | padding-top: 50px; 67 | background: var(--theme-bg_1); 68 | border-left: 3px solid var(--theme-primary); 69 | transform: translateX(100%); 70 | transition: transform 300ms; 71 | } 72 | 73 | .menu.isVisible { 74 | transform: translateX(0%); 75 | } 76 | 77 | .menu-btn { 78 | height: 30px; 79 | min-width: auto; 80 | min-height: auto; 81 | line-height: 30px; 82 | padding-top: 0; 83 | padding-bottom: 0; 84 | } 85 | 86 | .menu a { 87 | display: block; 88 | padding: 4px 0; 89 | } 90 | 91 | /* ------- CHECKBOX ------- */ 92 | 93 | .c-checkbox:hover + :last-child::before { 94 | border-color: #6f6f6f; 95 | } 96 | 97 | .c-checkbox input:focus + :last-child::before { 98 | border-color: #8c00ff; 99 | box-shadow: var(--theme-focus_shadow); 100 | outline: var(--theme-focus_outline); 101 | } 102 | 103 | .c-checkbox:last-child::before, 104 | .c-checkbox:last-child::after { 105 | display: inline-block; 106 | border-radius: 2px; 107 | } 108 | 109 | .c-checkbox :last-child::before { 110 | content: ""; 111 | display: inline-block; 112 | width: 14px; 113 | height: 14px; 114 | margin-right: 5px; 115 | border-radius: 2px; 116 | border: 1px solid #6f6f6f; 117 | transform: translateY(2px); 118 | transition: box-shadow 150ms, background-color 150ms; 119 | } 120 | 121 | .c-checkbox :last-child::after { 122 | font-family: sans-serif; 123 | content: "✓"; 124 | line-height: 1; 125 | font-size: 14px; 126 | position: absolute; 127 | width: 6px; 128 | height: 6px; 129 | top: 7px; 130 | left: 6px; 131 | color: #fff; 132 | transform: translate(-50%, -50%) scale(0); 133 | transition: transform 150ms; 134 | } 135 | 136 | .c-checkbox input:checked + :last-child::before { 137 | border-color: #8c00ff; 138 | background-color: #8c00ff; 139 | } 140 | 141 | .c-checkbox input:checked + :last-child::after { 142 | transform: translate(-50%, -50%) scale(1); 143 | } 144 | 145 | /* ------- OTHER STUFF ------- */ 146 | 147 | .toggleArea { 148 | display: flex; 149 | } 150 | 151 | .g-card { 152 | margin-bottom: 0; 153 | } 154 | 155 | .footer { 156 | text-align: center; 157 | padding-top: 0; 158 | } 159 | -------------------------------------------------------------------------------- /src/solutions/2.2.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Solution 2.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 68 |
69 | 70 |
71 |

72 | Hidden elements 73 |

74 | 75 | 76 |
77 | 83 |

Creating custom checkboxes is cool!

84 |
85 | 86 | 87 | 88 |
89 |
90 | 93 | 94 |
95 | Here's random article. 96 |
97 |
98 |
99 |

A dummy link for the demo.

100 |
101 | 102 | 103 | 131 | 132 | -------------------------------------------------------------------------------- /src/solutions/2.3.css: -------------------------------------------------------------------------------- 1 | @import url("2.2.css"); 2 | 3 | /* 💡 The .skipLink class */ 4 | /* 🍀 With :not() we don't need to override .sr-only styles when it's focused */ 5 | .skipLink:not(:focus) { 6 | width: 1px; 7 | height: 1px; 8 | padding: 0; 9 | margin: -1px; 10 | border: none; 11 | overflow: hidden; 12 | clip: rect(0, 0, 0, 0); 13 | border: 0; 14 | 15 | box-shadow: none; 16 | } 17 | 18 | .skipLink { 19 | position: fixed; 20 | top: 8px; 21 | left: 8px; 22 | display: inline-block; 23 | } 24 | 25 | .skipLink:focus { 26 | color: inherit; 27 | padding: 8px; 28 | border: 1px solid var(--theme-primary); 29 | background-color: var(--theme-bg_1); 30 | box-shadow: var(--theme-focus_shadow); 31 | outline: none; 32 | } 33 | 34 | /** Alternative without :not() 35 | .skipLink { 36 | position: fixed; 37 | top: 8px; 38 | left: 8px; 39 | 40 | width: 1px; 41 | height: 1px; 42 | padding: 0; 43 | margin: -1px; 44 | border: none; 45 | overflow: hidden; 46 | clip: rect(0, 0, 0, 0); 47 | } 48 | 49 | .skipLink:focus { 50 | display: inline-block; 51 | border: 1px solid var(--theme-primary); 52 | background-color: var(--theme-bg_1); 53 | color: inherit; 54 | padding: 8px; 55 | box-shadow: var(--theme-focus_shadow); 56 | 57 | width: auto; 58 | height: auto; 59 | margin: auto; 60 | overflow: auto; 61 | clip: none; 62 | } 63 | */ 64 | 65 | /* 💡 Bonus #1 */ 66 | /* CSS animation shown when the element 67 | receives focus from an href="#" */ 68 | #main:target { 69 | animation: tempRing 1500ms; 70 | } 71 | 72 | /* 💡 Bonus #2 */ 73 | .listTitle:focus { 74 | outline: none; 75 | animation: tempRing 1500ms; 76 | } 77 | 78 | /* ----------------------------------------- *\ 79 | |* You do not need to touch the code below *| 80 | |* but you can see how it was made. *| 81 | \** ----------------------------------------- */ 82 | 83 | .listBottom { 84 | display: flex; 85 | justify-content: flex-end; 86 | } 87 | 88 | .btnCircle svg { 89 | width: 16px; 90 | fill: currentColor; 91 | } 92 | 93 | @-webkit-keyframes tempRing { 94 | 30%, 95 | 75% { 96 | box-shadow: white 0 0 0 1px, #ef7979 0 0 0 3px; 97 | } 98 | } 99 | 100 | @keyframes tempRing { 101 | 30%, 102 | 75% { 103 | box-shadow: white 0 0 0 1px, #ef7979 0 0 0 3px; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/solutions/3.2.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | Solution #3.2 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 36 | 37 |
38 |

Page Landmarks

39 |
40 |

Good morning across the world

41 |

42 | In a world of around 6,000 languages, there are tons of ways to greet each other and say good morning. 43 | Here are three examples: 44 |

45 | 46 |
    47 |
  • 48 | 49 | Bonjour - French 50 |
  • 51 |
  • 52 | 53 | Bom dia - Portuguese (Portugal) 54 |
  • 55 |
  • 56 | 57 | Günaydin - Turkish 58 |
  • 59 |
60 | 61 | Learn to say "Good morning" in 62 | many languages. 63 |
64 | 65 | 66 |
67 | Nobody left a comment yet... 68 |
69 |
70 | 71 | 72 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /src/solutions/3.3_demo_exercise.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/solutions/3.3_demo_exercise.mp4 -------------------------------------------------------------------------------- /src/solutions/3.3_demo_solution.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandrina-p/workshop-a11y-fundamentals/81acdc526d0e095a7f81abd8a0dadede596c45a0/src/solutions/3.3_demo_solution.mp4 -------------------------------------------------------------------------------- /src/solutions/4.1.css: -------------------------------------------------------------------------------- 1 | /* 💡 BONUS 1/2 - Use aria-disabled */ 2 | .btnSubmit:hover:not([aria-disabled="true"]) { 3 | opacity: 0.8; 4 | } 5 | 6 | /* 💡 BONUS 2/2 - Use aria-disabled */ 7 | .btnSubmit[aria-disabled="true"] { 8 | opacity: 0.7; 9 | cursor: not-allowed; 10 | } 11 | 12 | /* ----------------------------------------- *\ 13 | |* You do not need to touch the code below *| 14 | |* but you can see how it was made. *| 15 | \** ----------------------------------------- */ 16 | 17 | .js-isHidden { 18 | display: none; 19 | } 20 | 21 | .area { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | padding-bottom: 24px; 26 | } 27 | 28 | .areaEnd { 29 | position: relative; 30 | text-align: right; 31 | } 32 | 33 | .field { 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | .fieldLabel { 39 | font-size: 1.8rem; 40 | font-weight: 600; 41 | margin-right: 4px; 42 | } 43 | 44 | .fieldInput { 45 | width: 50px; 46 | height: 40px; 47 | border: none; 48 | border: 1px solid var(--theme-text_1); 49 | border-radius: 4px; 50 | text-align: center; 51 | font-size: 1.8rem; 52 | } 53 | .fieldInput:focus { 54 | box-shadow: var(--theme-focus_shadow); 55 | outline: var(--theme-focus_outline); 56 | } 57 | .fieldInput::-webkit-outer-spin-button, 58 | .fieldInput::-webkit-inner-spin-button { 59 | -webkit-appearance: none; 60 | margin: 0; 61 | } 62 | .fieldInput[type="number"] { 63 | -moz-appearance: textfield; 64 | } 65 | 66 | .btnSubmit { 67 | --btnTxt: #fff; 68 | position: relative; 69 | display: inline-block; 70 | cursor: pointer; 71 | min-height: 44px; 72 | padding: 8px 20px; 73 | background-color: var(--theme-primary); 74 | border-radius: 4px; 75 | border: none; 76 | font-size: 1.8rem; 77 | color: var(--btnTxt); 78 | text-align: center; 79 | transition: color 250ms; 80 | } 81 | 82 | /* ... disabled styles moved to the top ... */ 83 | 84 | .btnSubmit:focus:not(:focus-visible) { 85 | outline: none; 86 | } 87 | 88 | .btnSubmit:focus-visible { 89 | box-shadow: var(--theme-focus_shadow); 90 | outline: var(--theme-focus_outline); 91 | } 92 | 93 | /* loading indicator */ 94 | .btnSubmit::after { 95 | content: ""; 96 | position: absolute; 97 | display: block; 98 | width: 0.7em; 99 | height: 0.7em; 100 | top: calc(50% - 0.5em); 101 | left: calc(50% - 0.5em); 102 | border: 2px var(--btnTxt); 103 | border-bottom-color: transparent; 104 | border-left-color: transparent; 105 | border-style: solid; 106 | border-radius: 50%; 107 | opacity: 0; 108 | transition: opacity 250ms; 109 | } 110 | 111 | .btnSubmit[data-loading="true"] { 112 | color: transparent; 113 | pointer-events: none; 114 | } 115 | .btnSubmit[data-loading="true"]::after { 116 | opacity: 1; 117 | animation: rotate 750ms linear infinite; 118 | } 119 | 120 | .btnSubmit[data-loading="true"] .btnSubmit-text { 121 | visibility: hidden; 122 | } 123 | 124 | .formStatus { 125 | position: absolute; 126 | top: 100%; 127 | right: 0; 128 | font-size: 1.3rem; 129 | color: green; 130 | margin-top: 6px; 131 | } 132 | 133 | .terms { 134 | position: relative; 135 | margin: 16px 0 0; 136 | text-align: center; 137 | } 138 | 139 | .tooltipArea { 140 | position: relative; 141 | } 142 | 143 | .tooltipBox { 144 | position: absolute; 145 | width: 104px; 146 | bottom: 100%; 147 | left: calc(50% - 52px); 148 | padding-bottom: 4px; /* use padding to preserve hover when moving cursor between the tooltip button and the tooltipItself */ 149 | 150 | opacity: 0; 151 | visibility: hidden; 152 | 153 | /* delay 250ms to give time to fade out */ 154 | transition: opacity 250ms, visibility 1ms 250ms; 155 | } 156 | 157 | .tooltipArea.isActive:hover .tooltipBox, 158 | .tooltipArea.isActive:focus-within .tooltipBox { 159 | opacity: 1; 160 | visibility: visible; 161 | transition: opacity 250ms; 162 | } 163 | .tooltipArea.isActive:hover .tooltipBox { 164 | /* delay fadein 500ms to prevent accidental hovers */ 165 | transition: opacity 250ms 500ms; 166 | } 167 | 168 | .tooltipItself { 169 | display: block; 170 | background: hsl(266deg 100% 15%); 171 | color: hsl(266deg 100% 96%); 172 | line-height: 1.2; 173 | padding: 6px 8px; 174 | font-size: 1.3rem; 175 | border-radius: 4px; 176 | text-align: center; 177 | -webkit-font-smoothing: initial; 178 | -moz-osx-font-smoothing: initial; 179 | } 180 | 181 | @keyframes rotate { 182 | 0% { 183 | transform: rotate(0deg); 184 | } 185 | 100% { 186 | transform: rotate(360deg); 187 | } 188 | } 189 | 190 | .resultsArea { 191 | width: 100%; 192 | min-height: 100px; 193 | border: 1px dotted var(--theme-primary); 194 | margin-top: 10px; 195 | padding: 10px; 196 | display: flex; 197 | align-items: center; 198 | justify-content: center; 199 | } 200 | -------------------------------------------------------------------------------- /src/solutions/4.1.react.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Solution #4.1 (React) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

A modern browser is needed to run this exercise with React.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/solutions/4.1.react.js: -------------------------------------------------------------------------------- 1 | /* 2 | 🍀 Some things to know about this: 3 | - Everything is fine with the form. Focus on the results area. 4 | - You can change the loading time at line 130. 5 | - The page "7" will throw a results error. 6 | */ 7 | 8 | function Exercise() { 9 | const [page, setPage] = React.useState(""); // input page value 10 | const [products, setProcuts] = React.useState([]); 11 | const [isLoadingProducts, setIsLoadingProducts] = React.useState(false); 12 | const [productsError, setProductsError] = React.useState(null); 13 | const isFormValid = page >= 1 && page <= 9; 14 | const isProductsOkay = !productsError && !isLoadingProducts; 15 | const isFormSubmitActive = isFormValid && !isLoadingProducts; 16 | 17 | async function handleSubmit(e) { 18 | e.preventDefault(); // avoid native form submit (page refresh) 19 | 20 | if (!isFormSubmitActive) { 21 | return false; 22 | } 23 | 24 | setIsLoadingProducts(true); 25 | setProductsError(null); 26 | 27 | try { 28 | const productsList = await fetchProducts(page); 29 | setProcuts(productsList); 30 | } catch (err) { 31 | const errorMessage = `Page failed. Technical error: ${err.message}`; 32 | setProductsError(errorMessage); 33 | } finally { 34 | setIsLoadingProducts(false); 35 | } 36 | } 37 | 38 | return ( 39 |
40 |

Loading states

41 | 42 |
43 |
44 |
45 |
46 | 49 | setPage(e.target.value)} 59 | /> 60 |
61 |
62 | 63 |
64 |
65 | 74 | 75 | 78 |
79 |
80 |
81 | 82 |
83 | {/* Empty State */} 84 | {products.length === 0 && isProductsOkay &&

No products yet.

} 85 | 86 | {/* Loading state */} 87 | {isLoadingProducts && ( 88 |

Loading products...

89 | )} 90 | 91 | {/* Error state */} 92 | {productsError &&

{productsError}

} 93 | 94 | {/* Products list */} 95 | {products.length > 0 && isProductsOkay && ( 96 |
97 | {/* 💡 Broadcast a summary of the loaded state */} 98 |

99 | {`Page ${page} loaded with ${products.length} products.`} 100 |

101 | 102 |
    103 | {products.map((product) => ( 104 |
  • Product {product.id}
  • 105 | ))} 106 |
107 |
108 | )} 109 |
110 | 111 |

112 | A{" "} 113 | 114 | dummy link 115 | {" "} 116 | for demo. 117 |

118 |
119 |
120 | ); 121 | } 122 | 123 | /* You can ignore the code below. */ 124 | 125 | async function fetchProducts(page) { 126 | console.log("Loading products from page:", page); 127 | 128 | if (page == 7) { 129 | // Simulate a problem with this page for demo purposes. 130 | await fakeWaitTime(500); 131 | throw Error("Page 7 is unstable."); 132 | } 133 | 134 | await fakeWaitTime(2500); 135 | 136 | // Return a dummy array of 10 items. Each item is an object. eg 137 | // [{ id: 20 }, { id: 21 }, { id: 22 }, { id: 23 }, ...] 138 | return Array.from(Array(10), (_, i) => ({ 139 | id: `${page}${i}`, 140 | })); 141 | } 142 | 143 | function fakeWaitTime(ms) { 144 | return new Promise((resolve) => setTimeout(resolve, ms)); 145 | } 146 | 147 | const rootElement = document.getElementById("root"); 148 | ReactDOM.render(, rootElement); 149 | -------------------------------------------------------------------------------- /src/solutions/4.2.css: -------------------------------------------------------------------------------- 1 | /* Control appearance based on data-* */ 2 | .stgs[data-theme="light"] { 3 | background: var(--theme-bg_1); 4 | color: var(--theme-text_0); 5 | } 6 | .stgs[data-theme="dark"] { 7 | background: var(--theme-text_0); 8 | color: var(--theme-bg_0); 9 | } 10 | 11 | .stgs[data-size="sm"] { 12 | font-size: 1.4rem; 13 | } 14 | .stgs[data-size="md"] { 15 | font-size: 1.6rem; 16 | } 17 | .stgs[data-size="lg"] { 18 | font-size: 1.9rem; 19 | } 20 | 21 | /* ---- Theme toggle ---- */ 22 | .stgs-toggleTheme { 23 | background: transparent; 24 | color: inherit; 25 | padding: 12px 16px; 26 | border: none; 27 | border-radius: 4px; 28 | border: 1px solid; 29 | } 30 | .stgs-toggleTheme:focus { 31 | box-shadow: var(--theme-focus_shadow); 32 | outline: var(--theme-focus_outline); 33 | } 34 | .stgs-toggleTheme::before { 35 | content: ""; 36 | margin-right: 8px; 37 | } 38 | .stgs-toggleTheme[aria-pressed="true"]::before { 39 | content: "✅"; 40 | content: "✅" / ""; 41 | } 42 | .stgs-toggleTheme[aria-pressed="false"]::before { 43 | content: "🔲"; 44 | content: "🔲" / ""; 45 | } 46 | 47 | /* ---- Sound toggle ---- */ 48 | .stgs-btnOnOff { 49 | background: transparent; 50 | color: inherit; 51 | padding: 12px 16px; 52 | border: none; 53 | border-radius: 4px; 54 | border: 1px solid; 55 | } 56 | .stgs-btnOnOff:focus { 57 | box-shadow: var(--theme-focus_shadow); 58 | outline: var(--theme-focus_outline); 59 | } 60 | .stgs-btnOnOff .on, 61 | .stgs-btnOnOff .off { 62 | display: none; 63 | } 64 | .stgs-btnOnOff[aria-pressed="true"] .on { 65 | display: block; 66 | } 67 | .stgs-btnOnOff[aria-pressed="false"] .off { 68 | display: block; 69 | } 70 | 71 | /* ---- Font size options ---- */ 72 | .stgs-toolbar { 73 | display: flex; 74 | flex-wrap: wrap; 75 | border: 0; 76 | padding: 0; 77 | } 78 | 79 | .stgs-toolbar-title { 80 | display: block; 81 | width: 100%; 82 | } 83 | .stgs-toolbar-item { 84 | border: none; 85 | border: 1px solid var(--theme-text_1); 86 | padding: 10px 10px; 87 | margin: 0; 88 | min-height: 44px; 89 | font-weight: 600; 90 | background: transparent; 91 | color: inherit; 92 | } 93 | 94 | .stgs-toolbar-item:focus { 95 | box-shadow: var(--theme-focus_shadow); 96 | outline: var(--theme-focus_outline); 97 | } 98 | 99 | .stgs-toolbar-item[aria-checked="true"] { 100 | background-color: var(--theme-primary); 101 | color: var(--theme-bg_1); 102 | } 103 | 104 | .stgs-toolbar-item[aria-disabled="true"] { 105 | opacity: 0.6; 106 | cursor: not-allowed; 107 | } 108 | -------------------------------------------------------------------------------- /src/solutions/4.2.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Solution #4.2 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

On-demand changes

20 | 21 |
22 | 23 |
24 | 29 | 32 |
33 | 34 | 35 |
36 | 42 | 51 |
52 | 53 | 54 | 55 |
56 | 63 |
64 | Text size 65 | 68 | 71 | 74 | 77 |
78 | 79 |

Check a Toolbar 80 | example, 81 | following WAI ARIA practices.

82 |
83 |
84 |
85 | 86 | 150 | 151 | 152 | --------------------------------------------------------------------------------