HTML and CSS course Β· Module 10: Accessibility (a11y)

Project: Accessible Egyptian Museum Page

8 min read
In this lesson5

You already know every element of an accessible page, but for now they lie separately, like stone blocks on a building site. The real test begins when you have to assemble them into a whole pyramid and check that all the corridors connect. Time to combine everything we have learned about accessibility in one project! We will create an Ancient Egypt Museum page that is fully accessible - meeting WCAG 2.1 standards at the AA level.

Project Elements

Our page will contain:

  1. Skip link - skipping to the main content
  2. Semantic structure - header, nav, main, aside, footer
  3. Accessible navigation - with aria-label and aria-current
  4. Exhibition section - with accessible images (alt, figure, figcaption)
  5. Reservation form - with labels, fieldset, legend, validation
  6. Multimedia section - with captions and transcript
  7. Contrast and typography - meeting WCAG AA
  8. Keyboard navigation - visible focus, logical Tab order
  9. ARIA attributes - aria-live, aria-expanded, aria-describedby

HTML Structure

We start building from the top. lang="en" tells the screen reader which language to read in, and the skip link must be the first element in <body>, so that Tab reaches it first:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Ancient Egypt Museum - Accessible Page</title>
7</head>
8<body>
9    <!-- 1. Skip link -->
10    <a href="#main-content" class="skip-link">
11        Skip to main content
12    </a>
13
14    <!-- 2. Header with navigation -->
15    <header role="banner">
16        <h1>Ancient Egypt Museum</h1>
17        <nav aria-label="Main navigation">
18            <ul>
19                <li><a href="#exhibitions" aria-current="page">Exhibitions</a></li>
20                <li><a href="#reservation">Reservation</a></li>
21                <li><a href="#multimedia">Multimedia</a></li>
22                <li><a href="#contact">Contact</a></li>
23            </ul>
24        </nav>
25    </header>

The role="banner" attribute is redundant here, because a <header> at the top level of the page has this role built in - you can remove it in your own project. The navigation got the name "Main navigation", because a second one will appear in the footer.

Step 2: the exhibition section

The main content gets id="main-content", which is the skip link's target. The exhibition section has its own heading, and aria-labelledby gives the section a name taken from that heading:

1    <!-- 3. Main content -->
2    <main id="main-content">
3        <!-- Exhibition section -->
4        <section aria-labelledby="exhibitions-title">
5            <h2 id="exhibitions-title">Current Exhibitions</h2>
6
7            <article>
8                <h3>Treasures of Tutankhamun</h3>
9                <figure>
10                    <img src="tutankhamun-mask.jpg"
11                         alt="Golden death mask of pharaoh Tutankhamun">
12                    <figcaption>
13                        Mask of Tutankhamun, XVIII Dynasty, c. 1323 BCE.
14                    </figcaption>
15                </figure>
16                <p>Discover extraordinary artifacts from the tomb of the young pharaoh.</p>
17            </article>
18        </section>

Thanks to its name, the section becomes a "Current Exhibitions" landmark. The heading hierarchy is continuous: <h1> in the page header, <h2> for the section and <h3> for the exhibition.

Step 3: the reservation form

The form brings together everything from the forms lesson - labels, field groups, descriptions and autocomplete:

1        <!-- Reservation form -->
2        <section aria-labelledby="reservation-title">
3            <h2 id="reservation-title">Book a Visit</h2>
4            <form aria-labelledby="reservation-title" novalidate>
5                <fieldset>
6                    <legend>Personal Information</legend>
7                    <div class="form-group">
8                        <label for="name">Full name:</label>
9                        <input type="text" id="name"
10                               autocomplete="name"
11                               aria-required="true">
12                    </div>
13                    <div class="form-group">
14                        <label for="email">Email:</label>
15                        <input type="email" id="email"
16                               autocomplete="email"
17                               aria-required="true"
18                               aria-describedby="email-help">
19                        <p id="email-help" class="hint">
20                            We will send the booking confirmation to this address.
21                        </p>
22                    </div>
23                </fieldset>
24
25                <fieldset>
26                    <legend>Choose an Exhibition</legend>
27                    <label>
28                        <input type="radio" name="exhibition"
29                               value="tutankhamun">
30                        Treasures of Tutankhamun
31                    </label>
32                    <label>
33                        <input type="radio" name="exhibition"
34                               value="mummies">
35                        Royal Mummies
36                    </label>
37                </fieldset>
38
39                <button type="submit">Book</button>
40            </form>
41        </section>
42    </main>

Every field has a label with a matching for, the radio group has its own <fieldset> with a <legend>, and the hint below the email is connected to the field with aria-describedby.

Finally, the elements outside the main content - opening hours in an <aside> and a footer with a second navigation:

1    <!-- 4. Sidebar -->
2    <aside aria-label="Additional information">
3        <h2>Opening Hours</h2>
4        <dl>
5            <dt>Monday - Friday</dt>
6            <dd>9:00 - 17:00</dd>
7            <dt>Saturday - Sunday</dt>
8            <dd>10:00 - 18:00</dd>
9        </dl>
10    </aside>
11
12    <!-- 5. Footer -->
13    <footer>
14        <p>Ancient Egypt Museum 2024</p>
15        <nav aria-label="Footer navigation">
16            <ul>
17                <li><a href="/privacy-policy">Privacy Policy</a></li>
18                <li><a href="/accessibility">Accessibility Statement</a></li>
19            </ul>
20        </nav>
21    </footer>
22</body>
23</html>

The opening hours are a definition list, because they pair a day with hours. The second navigation has a different name, so the reader can tell it apart from the main one, and a link to an accessibility statement is a good habit - public sector bodies in Poland are required to publish one.

Step 5: dynamic messages

The requirements also include ARIA attributes for content that changes on the page. Add a region for the message shown after booking and a button that expands the opening hours:

1<!-- Message after the form is submitted -->
2<p id="form-status" aria-live="polite"></p>
3
4<!-- Button that expands additional information -->
5<button type="button" aria-expanded="false" aria-controls="hours">
6    Show opening hours
7</button>

The aria-live region sits on the page from the start and is empty - the reader announces text only when a script writes it there. The script also switches aria-expanded to "true" after expanding, and aria-controls points to the id of the expanded element.

CSS Styles - Accessibility

In CSS we start with variables holding high-contrast colors, then hide the skip link and design a visible focus outline:

1/* Color variables with high contrast */
2:root {
3    --text-primary: #1a1a2e;
4    --text-secondary: #4a4a5a;
5    --bg-primary: #ffffff;
6    --bg-secondary: #f5f1e3;
7    --accent-gold: #8b6914;
8    --accent-hover: #6b4c00;
9    --error: #d32f2f;
10    --success: #2e7d32;
11    --focus-color: #d4af37;
12}
13
14/* Skip link */
15.skip-link {
16    position: absolute;
17    top: -50px;
18    left: 16px;
19    background: var(--text-primary);
20    color: var(--bg-primary);
21    padding: 12px 24px;
22    z-index: 1000;
23    font-size: 1rem;
24    border-radius: 0 0 4px 4px;
25    transition: top 0.2s;
26}
27
28.skip-link:focus {
29    top: 0;
30}
31
32/* Visible focus for all interactive elements */
33a:focus-visible,
34button:focus-visible,
35input:focus-visible,
36select:focus-visible,
37textarea:focus-visible {
38    outline: 3px solid var(--focus-color);
39    outline-offset: 2px;
40}

Check one thing yourself, though: the gold outline #d4af37 reaches only about 2.1:1 on a white background, while WCAG requires at least 3:1 for interface states. On a light background the darker --accent-gold (about 5:1) works better - good proof that ready-made code is always worth testing.

Step 7: typography and user preferences

The last part of the stylesheet sets readable typography and reacts to three system settings: reduced motion, more contrast and dark mode:

1/* Accessible typography */
2body {
3    font-family: Georgia, 'Times New Roman', serif;
4    font-size: 100%;
5    line-height: 1.6;
6    color: var(--text-primary);
7    background: var(--bg-primary);
8}
9
10/* Text width 50-80 characters */
11main {
12    max-width: 70ch;
13    margin: 0 auto;
14    padding: 1rem;
15}
16
17/* Prefers reduced motion */
18@media (prefers-reduced-motion: reduce) {
19    * {
20        animation-duration: 0.01ms !important;
21        transition-duration: 0.01ms !important;
22    }
23}
24
25/* Prefers contrast */
26@media (prefers-contrast: more) {
27    :root {
28        --text-primary: #000000;
29        --bg-primary: #ffffff;
30        --accent-gold: #6b4c00;
31    }
32}
33
34/* Dark mode */
35@media (prefers-color-scheme: dark) {
36    :root {
37        --text-primary: #e0d5c0;
38        --bg-primary: #1a1a2e;
39        --bg-secondary: #2d2d44;
40    }
41}

The media queries only swap variable values, so the rest of the stylesheet stays untouched. A font size in percent and a 70ch column make the page cope well with zooming to 200%.

Project Checklist

After completing the project, check:

  • Skip link works and is visible on focus
  • Semantic structure (header, nav, main, aside, footer)
  • Navigation with aria-label and aria-current
  • All images have alt text
  • Form has a label for every field
  • Fieldset and legend for field groups
  • aria-required and aria-describedby in the form
  • Color contrast minimum 4.5:1
  • Visible focus on all elements
  • Page works at 200% zoom
  • prefers-reduced-motion supported
  • prefers-contrast supported
  • prefers-color-scheme supported
  • aria-live for messages and aria-expanded for expandable elements

You will check every point with the tools from the testing lesson: Lighthouse and axe to start, then the keyboard, zoom and a screen reader. Send the link to your page together with a short description of the techniques you used.

Congratulations! You have created a page that is truly accessible to everyone - just as the ancient temples of Egypt were open to every pilgrim. Mohamed would be proud! In the next module you will discover tools that speed up your work - from DevTools to publishing a page on the internet.

Practice

In the editor below you will find a complete version of the project. Go through it with the checklist in hand before you build your own:

Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Ancient Egypt Museum - Accessible Page</title>
7    <link rel="stylesheet" href="style.css">
8</head>
9<body>
10    <!-- Skip link -->
11    <a href="#main-content" class="skip-link">
12        Skip to main content
13    </a>
14
15    <!-- Header -->
16    <header role="banner">
17        <div class="header-inner">
18            <h1>
19                <span aria-hidden="true">&#9653;</span>
20                Ancient Egypt Museum
21            </h1>
22            <nav aria-label="Main navigation">
23                <ul>
24                    <li><a href="#exhibitions" aria-current="page">Exhibitions</a></li>
25                    <li><a href="#reservation">Booking</a></li>
26                    <li><a href="#gallery">Gallery</a></li>
27                    <li><a href="#contact">Contact</a></li>
28                </ul>
29            </nav>
30        </div>
31    </header>
32
33    <!-- Main content -->
34    <main id="main-content">
35        <!-- Exhibitions section -->
36        <section id="exhibitions" aria-labelledby="ex-title">
37            <h2 id="ex-title">Current exhibitions</h2>
38
39            <article>
40                <h3>Treasures of Tutankhamun</h3>
41                <figure>
42                    <img src="https://placehold.co/600x350/d4af37/1a1a2e?text=Tutankhamun+Mask"
43                         alt="Golden death mask of pharaoh Tutankhamun with lapis lazuli inlays">
44                    <figcaption>
45                        Death mask of Tutankhamun, 18th dynasty, c. 1323 BCE
46                    </figcaption>
47                </figure>
48                <p>Discover over 5000 extraordinary artifacts from the tomb
49                   of the young pharaoh, discovered in 1922.</p>
50                <a href="#reservation" class="btn">Book a visit</a>
51            </article>
52
53            <article>
54                <h3>Royal Mummies</h3>
55                <p>Discover the secrets of mummification and see preserved
56                   mummies of the pharaohs from the Valley of the Kings.</p>
57                <a href="#reservation" class="btn">Book a visit</a>
58            </article>
59        </section>
60
61        <!-- Reservation form -->
62        <section id="reservation" aria-labelledby="res-title">
63            <h2 id="res-title">Book a visit</h2>
64
65            <form aria-labelledby="res-title" novalidate>
66                <fieldset>
67                    <legend>Personal information</legend>
68
69                    <div class="form-group">
70                        <label for="name">
71                            Full name
72                            <span class="required" aria-hidden="true">*</span>
73                        </label>
74                        <input type="text" id="name"
75                               autocomplete="name"
76                               aria-required="true">
77                    </div>
78
79                    <div class="form-group">
80                        <label for="email">
81                            Email address
82                            <span class="required" aria-hidden="true">*</span>
83                        </label>
84                        <input type="email" id="email"
85                               autocomplete="email"
86                               aria-required="true"
87                               aria-describedby="email-help">
88                        <p id="email-help" class="hint">
89                            We will send the confirmation to this address.
90                        </p>
91                    </div>
92                </fieldset>
93
94                <fieldset>
95                    <legend>Choose an exhibition</legend>
96                    <label class="radio-label">
97                        <input type="radio" name="exhibition" value="tutankhamun">
98                        Treasures of Tutankhamun
99                    </label>
100                    <label class="radio-label">
101                        <input type="radio" name="exhibition" value="mummies">
102                        Royal Mummies
103                    </label>
104                </fieldset>
105
106                <fieldset>
107                    <legend>Visit details</legend>
108                    <div class="form-row">
109                        <div class="form-group">
110                            <label for="visit-date">Visit date:</label>
111                            <input type="date" id="visit-date" aria-required="true">
112                        </div>
113                        <div class="form-group">
114                            <label for="visitors">Number of people:</label>
115                            <input type="number" id="visitors" min="1" max="20" value="1">
116                        </div>
117                    </div>
118                </fieldset>
119
120                <button type="submit" class="btn btn-primary">
121                    Book a visit
122                </button>
123            </form>
124        </section>
125    </main>
126
127    <!-- Sidebar -->
128    <aside aria-label="Practical information">
129        <h2>Opening hours</h2>
130        <dl>
131            <dt>Monday - Friday</dt>
132            <dd>9:00 - 17:00</dd>
133            <dt>Saturday - Sunday</dt>
134            <dd>10:00 - 18:00</dd>
135        </dl>
136
137        <h2>Prices</h2>
138        <dl>
139            <dt>Standard ticket</dt>
140            <dd>$45</dd>
141            <dt>Reduced ticket</dt>
142            <dd>$25</dd>
143            <dt>Family ticket</dt>
144            <dd>$100 (2+2)</dd>
145        </dl>
146    </aside>
147
148    <!-- Footer -->
149    <footer>
150        <nav aria-label="Footer navigation">
151            <ul>
152                <li><a href="/privacy">Privacy policy</a></li>
153                <li><a href="/accessibility">Accessibility statement</a></li>
154                <li><a href="/sitemap">Sitemap</a></li>
155            </ul>
156        </nav>
157        <p>Ancient Egypt Museum &copy; 2024. All rights reserved.</p>
158    </footer>
159</body>
160</html>

Remember: accessibility is not a decoration added at the end, but the foundation of the pyramid - build it from the first block, and every pilgrim will reach the pharaoh's chamber.

Spotted a mistake in this lesson?

Useful articles