How to Code an Accessible Website with HTML & CSS (by Example)
In this play guide, I'll show you step by step how to code an accessible website with HTML and
CSS
so it's useable by everyone!
Including visually impaired users.
The UI design we'll code is the footer block of codingnepalweb.com.
It was a challenge by a mentee of the Google Africa Developer Scholarship program, Mobile Web Specialist Track.
I decided to share my thought process in coding this UI in 3 stages.

In the end, you can compare the result you'll achieve to that of the live website.
You'll be amazed at how much you've learned to make the code wayyy better for both user experience, and developer experience.
To follow along, you need any code editor of your choice, such as VsCode. Or, a code playground like Codepen / Glitch.
If you have a Screen Reader, like NVDA, then you'll hear the benefits of semantic HTML that you'll write.
If you don't have one installed, I've got you! I provided illustrated video clips of how NVDA reads the code so you can hear for yourself.
Let's shoot.
Stage 1: Create the Markup with Semantic HTML
Semantic HTML is the use of the right tag to markup the content.
For example:
If you've got a sentence, then use the paragraph tag <p>
to markup
that
sentence.
But if that sentence is a quote, use the blockquote tag <blockquote>
instead.
You get the gist.
This difference will help with content formatting and pronounciation, the right way.
Now, let's create our markup starting from the beginning.
-
Structure the HTML Document
Set up the arena by firing up your code editor and create an HTML file with the content below.
<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="UTF-8" > <!-- Metadata --> </head> <body> <!-- Content --> </body> </html>
By that, we've made an empty webpage whose content language will be English (
lang="en"
).With a direction of left-to-right (
dir="ltr"
).And the character encoding of choice is
UTF-8
.The
<head>
section will contain other metadata while the<body>
section will contain the contents. -
Outline the Document's Content
The Document Outline shows the relationship between content parts of a webpage, nested or flat.
This hierarchical structure is conveyed through the 6 HTML heading elements
h1
-h6
.Even though these heading elements are written one after another, the content that follows each is nested logically.
Hence, the need to carefully convey the hierarchy of content parts.
Looking at the mockup, we have 3 bold standalone texts that fit to be headlines.
Possible headings are circled with purple color If we were to arrange them in a tree-like structure, we'll have:
-
CodingNepal
- About Us
- Follow Us
This nesting makes sense for the little content above. The "About Us" and "Follow Us" blocks are describing the Brand - CodingNepal.
To convey the hierarchy to Audial Users using a Screen Reader, we need to use the right heading level.
There's one rule: Don't skip a heading level.
Following that rule, we'll have:
<body> <h1>CodingNepal</h1> <h2>About Us</h2> <h2>Follow Us</h2> </body>
If we'd start with a different heading level other than
h1
, or skip a heading level (ex. jumping fromh1
toh3
), visually this would be fine. But to Screen Readers, it'll raise concerns for missing headlines (where ish2
?).The image below shows how browser default styles conveyed each heading level by size. To a screen reader, it's a tree-like structure. (See the overlay of the document outline generated using HTML5 Outliner .)
With the headlines sorted, let's mark up the paragraph(s) that follow.
-
CodingNepal
-
Mark up the About Us Block
For the "About Us" block, we'll use the paragraph element
p
to markup the description.And an
address
to state the contact information.To allow users to send messages with a click, we'll use the anchor element
a
with amailto
scheme in itshref
attribute.The anchor text will be the email address so visual users can copy it for use elsewhere.
But to screen readers, communicating the link's purpose is better than just stating the email address.
We'll override stating the email address by using the
aria-label
attribute with the value of "write us a message".Two vocals convey how a mailto link is pronounced. The first reads out the anchor text - the email address. The second reads the purpose of the link, provided with aria-label
(This sounds better.)Finally, let's group the headline, paragraph, and address elements together with an
article
element. As they form an independent, self-contained block.The markup should look like this:
<h1>CodingNepal</h1> <article> <h2>About Us</h2> <p>CodingNepal is a blog where we post blogs related to HTML, CSS, JavaScript & PHP along with creative coding stuff.</p> <address>Contact Us: <a href="mailto:email@example.com" aria-label="Write us a message">email@example.com</a></address> </article> <h2>Follow Us</h2>
And here's a screenshot of the result with the document outline still intact.
-
Mark up the Follow Us Block
The "Follow Us" block is a list of links to pages outside of the website.
For quick results, let's use font-awesome for each icon. This will require us to include the CDN link in the
head
section of the HTML document.Then, we display each icon with its name in the
class
attribute of a<span>
tag.<html lang="en" dir="ltr"> <head> <!-- Load Font icons --> <link rel="stylesheet" href="<https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css>"> </head> <body> <!-- ... --> <h2>Follow Us</h2> <span class="fab fa-facebook-f"></span> <span class="fab fa-instagram"></span> <span class="fab fa-twitter"></span> <span class="fab fa-youtube"></span> </body> </html>
Next, we'll wrap each icon with an anchor link element
a
. So, they're clickable with mouse, tappable with touch, and focusable with the keyboard.Since this link contains no text, we'll provide one with the
aria-label
attribute for screen readers to hear.<h2>Follow Us</h2> <a href="#" aria-label="Facebook Page"> <span class="fab fa-facebook-f"></span> </a> <!-- ... -->
Two vocals convey how an anchor link with no text is read out. The first defaults to the url parts. The second used the aria-label
text, which sounds better.Then, we'll wrap each anchor link with a list item.
To the left of the image below is what the code should look like. To the right is the result with each icon now having a bullet list.
This action communicates the links better to Screen Readers. It also announces the total links.
The first vocal reads out a bunch of links. The second vocal sounds better because each link was wrapped with a list item. Later on, we'll use CSS to remove these bullets so it doesn't interfere with our styling.
However, this action can prevent some screen readers to ignore reading the links as a list.
To ensure the list is always communicated, we'll specify a
role
attribute with a value of "list" to the<ul>
tag.<ul role="list"> <!-- ... --> </ul>
Also, we'll describe the purpose of this list as Social Links using the
aria-label
attribute on the<ul>
tag.The first vocal pronounced the list block as-is. The second pronounced the list block by its purpose, through the aria-label
attribute.We should be having the codes structured like below
<h2>Follow Us</h2> <ul role="list" aria-label="Social Links"> <li> <a href="#" aria-label="Facebook Page"> <span class="fab fa-facebook-f"></span> </a> </li> <!-- ... --> </ul>
Like we did earlier, we'll group the headline and its list content using an
article
element.<article> <h2>Follow Us</h2> <ul role="list" aria-label="Social Links"> <!-- ... --> </ul> </article>
And here's how the results, so far.
If you wonder why we aren't using the navigation element
nav
, here's a quote from w3c.Note: Not all group of links on a page need to be in a nav element — the element is primarily intended for sections that consist of major navigation blocks.
Major navigation blocks include the site-wide navigation, search form, or table of contents.
-
Identify the Region
The
h1
and 2article
blocks we marked up above have one thing in common. They provide concluding information for the page.As such, we will group them with a
footer
element. With its parent being abody
element, the footer becomes the page's contentinfo landmark.Thereby making this region navigatable by Screen Readers for easy access to all of its blocks.
<footer> <h1> <!-- ... --> </h1> <article> <!-- ... --> </article> <article> <!-- ... --> </article> </footer>
-
Prepare the Markup for Responsiveness
The width of a webpage are dynamic by default. On desktops, it's equal to the browser window. On Mobile, it's set to around 980px.
If we do nothing, the 980px webpage width will be scaled down to fit the mobile screen. Thereby, rendered smaller.
In the left device, the webpage is scaled out because there is no viewport meta tag. In the right device, there is a viewport meta tag that specified the width to equal the device-width. This makes contents to be viewed at their normal size. To render the webpage appropriately, like the right device in the image above, we'll need to specify a
meta
element with aname
ofviewport
.Its
width
property will be equal to thedevice-width
and theinitial-scale
will be1
.<head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- ... -->
-
Prepare Elements for Styling
First, let's ensure our HTML markup is ready for CSS layout.
On looking at the mockup, you'd realize the contents are visually arranged into 3 blocks. All in one big dark-colored container.
In our code, we have
h1
, 2article
s, andfooter
elements as equivalent blocks.Hence, no need for any
div
element to wrap contents. -
Name Elements for Styling
As you've heard, naming things is hard. It's true if you decide to name every Tom, Dick, and Harry of an element you have.
Instead, name components and target their elements using native CSS selectors. Life becomes easier!
In our case, we'll name the
footer
element withpage-footer
,h1
element withfooter-heading
, and the 2article
elements withabout-info
, andsocial-links
.Other elements are targeted through their parent. As simple as that.
Here's the markup with each name in its element's class attribute.
<footer class="page-footer"> <h1 class="footer-heading"> <!-- ... --> </h1> <article class="about-info"> <!-- ... --> </article> <article class="social-links"> <!-- ... --> </article> </footer>
This step concludes the markup stage.
Below is how NVDA, a screen reader, reads the webpage. All elements are communicated better. Such as the region, purpose of links, and total links.
Here's the code so far.
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Load Font icons -->
<link rel="stylesheet" href="<https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css>">
</head>
<body>
<footer class="page-footer">
<h1 class="footer-heading">CodingNepal</h1>
<!-- About Us Block -->
<article class="about-info">
<h2>About Us</h2>
<p>CodingNepal is a blog where we post blogs related to HTML, CSS, JavaScript & PHP along with creative coding stuff.</p>
<address>Contact Us: <a href="mailto:email@example.com" aria-label="Write us a message">email@example.com</a></address>
</article>
<!-- Follow Us Navigation Block -->
<article class="social-links">
<h2>Follow Us</h2>
<ul role="list" aria-label="Social Links">
<li>
<a href="#" aria-label="Facebook Page">
<span class="fab fa-facebook-f"></span>
</a>
</li>
<li>
<a href="#" aria-label="Instagram Page">
<span class="fab fa-instagram"></span>
</a>
</li>
<li>
<a href="#" aria-label="Twitter Page">
<span class="fab fa-twitter"></span>
</a>
</li>
<li>
<a href="#" aria-label="Youtube Page">
<span class="fab fa-youtube"></span>
</a>
</li>
</ul>
</article>
</footer>
</body>
</html>
Onwards, we'll be styling with CSS. This is where we make things beautiful without sacrificing accessibility.
Stage 2: Style the Page Components
Component Design allows us to style HTML blocks in isolation. This creates a separation of concern between how the block looks, and where it is laid out.
Let's break down the page into 5 components:
We'll have the Root Component that houses everything.
And then the Footer Component that sticks to the bottom of the Root Component. And containing 3 blocks: the Footer Heading, "About Us" block, and "Social Links" Block.
-
Style the Root Component
The root component is the perfect place to declare the font styles of the webpage so it can respond to user's choice in the browser settings.
We'll set the
font-size
property ofhtml
selector to100%
of the browser's font size.And, use font relative units such as
rem
, andem
on any element that needs to adapt to user's changes.Along with this, we'll specify a
line-height
property to make text lines easier to read. Along with afont-family
set tointer, sans-serif
.Since inter is a web font, we need to request it using the
link
element in thehead
section of the HTML file.<head> <!-- ... --> <!-- Load font family --> <link rel="stylesheet" href="<https://fonts.googleapis.com/css?family=Inter>"> <style> html { font-size: 100%; line-height: 1.8; font-family: inter, sans-serif; } </style> </head> <body> <!-- ... -->
The device in the right of the image below applied the styles above. Notice how well spaced it is when compared to the device at the left.
-
Style the Footer Component
For the
.page-footer
theme, I'm using a 3-digit hex codes for the greys -#ddd
and#222
.It's tinted with blue by raising the last character one step to get
#dde
and#223
.If I want a greenish grey, I will raise the middle number to get
#ded
and#232
. Same for Reddish grey with#edd
and#322
.Any of these pairs will pass the WCAG Level AAA contrast ratio for normal text, to aid readability.
Also, we'll pad the blocks from touching the edges of the footer block.
/* Page Footer Component */ .page-footer { color: #dde; background: #223; padding: 1.5rem 1.5rem 2rem; }
Here's how it looks.
One more thing:
Let's stick the Footer to the bottom of the viewport.Illustration showing how a footer
sticks to the bottom of the viewport as the height changes.Here are 3 steps to create a sticky footer using CSS Flexbox.
- Set the html, and body elements to take 100% height of the viewport
- Specify a display of flex to .page-footer's parent. And a flex-direction of column.
- Use a margin-top of auto on the .page-footer to push contents above it further away
/* For Sticky Footer */ html, body { height: 100%; } body { margin: 0; display: flex; flex-direction: column; } .page-footer { margin-top: auto; }
Below is the result of the above code.
The footer sticks to the bottom of the page even though there're no elements above it. -
Style the Footer Heading Component
The
.footer-heading
is styled with afont-size
of2.441rem
. A value from this Type scale tool to create a visual hierarchy for font sizes based on 16px.Since we used the
rem
unit, thisfont-size
will adapt to changes made by users through the browser's font size. There's also aline-height
with a value of1.2
.Also, is a
margin
property, specified to distance the headline from other blocks. This spacing will create a sense of the relationship between blocks./* Footer Heading Component */ .footer-heading { font-size: 2.441rem; line-height: 1.2; margin: 0.5em 0 1em; }
And here's how the Heading block looks like.
-
Style The About Us Component
Textual blocks like the "About Us" can expand to fill any available width.
We need to restrict thier width to aid readabilty. In the context of the footer, let's use a
max-width
of20em
.For the contact link, we'll style its state when hovered with the mouse, and focused with a keyboard.
/* About Us Component */ .about-info { max-width: 20em; margin-bottom: 2em; } .about-info a { text-decoration: none; color: deepskyblue; } /* Focus Styles for Mouse & Keyboard Users */ .about-info a:hover, .about-info a[href]:focus-visible { outline: 2px dashed orange; outline-offset: 4px; } /* However, Dont show focus on mouse click */ .about-info a[href]:focus:not(:focus-visible) { outline: none; } .about-info p { margin: 0 0 0.75em; } .about-info > h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: 0.12rem; margin-bottom: 0.75rem; }
Also, we targeted the h2 child element of
.about-info
block using the child selector ">". Made the headline all caps as per the mockup and letter spaced it to aid caps readability.Because of the All Caps style, NVDA will pronounce the headline as "About U.S." (as in the United States) instead of "About Us". That can be misleading.
The video below shows the difference in pronounciation.
We'll fix this using
aria-label
attribute with value of "About Us" on theh2
element. Make sure the value is in capitalized form or small caps.<!-- aria-label is included to avoid mispronunciation when styled with all caps --> <h2 aria-label="About Us">About Us</h2>
Below is how the "About Us" block looks like. The dotted line denotes the
20em
mark. -
Style the Social Links Block
Since we marked up the Social links with an unordered list
ul
, we'll need to remove its markers, margin, and padding. Then, style each link to look like we want.For each link, we'll remove its default underline using a
text-decoration
ofnone
. Specify awidth
andheight
property.center
the icon vertically usingalign-items
and horizontally usingjustify-content
.Note that the centering will only work if we set a
display
offlex
on the anchor link selector.Also, we specified the color for each link block at their default state, when hovered, and focused.
/* Social Navigation Menu Component */ /* Remove markers, padding, and margin on ul */ .social-links > h2 { font-size: 1rem; text-transform: uppercase; letter-spacing: 0.12rem; margin-bottom: 1.25rem; } .social-links > ul { list-style: none; padding: 0; margin: 0; display: flex; } .social-links a { text-decoration: none; display: flex; justify-content: center; align-items: center; width: 2em; height: 2em; margin-right: 1rem; border: 1px solid #445; background: #334; color: white; } .social-links li:last-child a { margin-right: 0; } .social-links a:hover, .social-links a[href]:focus-visible { outline: 1px dashed orange; color: orange; } .social-links a[href]:focus:not(:focus-visible) { outline: none; }
Similarly, we need to include aria-label with a value of "Follow Us" (in non-capitalized form) on the heading element. So that 'US' isn't pronounced as 'U.S.'.
<!-- Aria-label is included to avoid mispronunciation when styled with all caps --> <h2 aria-label="Follow Us">Follow Us</h2>
Here's how the "Follow Us" block looks like with a focus on one of the link blocks.
This step concludes the Component Styling Stage.
Stage 3: Create Responsive Designs
Responsive Web Design (RWD) is all about making contents of a webpage look good at any device screen that views it.
RWD can be achieved by moving contents around when there's limited space through CSS Layouts. Or restricting contents from growing larger. Or hiding contents if neccessary.
To detect when to take an action for a particular screen, we can use the Media Query rule.
Let's start with our webpage responsive for mobile screens.
We'll be using CSS Flexbox for all of our layouts.
-
Design for Mobile Screens
Currently, each block stacks on top of another and aligns to the left of the viewport.
That's not looking bad until we have a slightly larger screen. With everything aligning to the left, the view will lack some sense of balance.
To make the contents look balance on multiple mobile screens, we'll center all blocks and their content by doing the following:
- Maintain all blocks in a single
column
usingflex-direction
property. - Align each block to the
center
usingalign-items
- Align all text to
center
usingtext-align
property.
For the properties
flex-direction
andalign-items
to work, we need to specify adisplay
offlex
./* For Mobile View */ .page-footer { display: flex; flex-direction: column; align-items: center; text-align: center; }
Below is how the mobile view looks
For slightly larger screens, the contents will still maintain their balance.
- Maintain all blocks in a single
-
Design for Medium-Sized Screens
As the screen grows wider, the spaces around the blocks make the view look scanty.
Let's fill up the space by rearranging the blocks. We'll make the last two blocks be at the bottom row.
We'll start by declaring some properties for the
.page-footer
selector. Where:- All 3 blocks are on the same
row
usingflex-direction
property. - The
.footer-heading
takes awidth
of100%
. Pushing the other two blocks to a new row when wewrap
blocks usingflex-wrap
on the.page-footer
. - We will distribute the
space-around
these two blocks usingjustify-content
property
/* For Medium Screen View (ex. Tablets) */ .footer-heading { width: 100%; } .page-footer { flex-direction: row; flex-wrap: wrap; justify-content: space-around; }
But first, we need to reset some
.page-footer
properties we declared for mobile screens. This includes resettingtext-align
back toleft
, andalign-items
tostart
..page-footer { flex-direction: row; flex-wrap: wrap; justify-content: space-around; /* Reset rules declared on mobile */ text-align: left; align-items: start; }
Also, I'll reset the
margin-bottom
of the "Follow Us" headline to1rem
. This aligns the link blocks to the Cap height of the "About Us" Sentence..social-links > h2 { margin-bottom: 1rem; }
It feels good to my eyes 😍
Finally, we'll use a media query rule to allow these selectors and their properties to take effect at a certain width.
This is the width where the "About Us" and "Follow Us" blocks start to look better on the same row. I'm okay with the view starting at 600px screen width.
Here's how the whole styles look when wrapped in a media query rule.
/* For Medium Screen View (ex. Tablets) */ @media screen and (min-width: 600px) { .footer-heading { width: 100%; } .page-footer { flex-direction: row; flex-wrap: wrap; justify-content: space-around; /* Reset rules declared on mobile */ text-align: left; align-items: start; } .social-links > h2 { margin-bottom: 1rem; } }
- All 3 blocks are on the same
-
Design for Large screens
If you widen the screen further, the spaces around those blocks will become too much.
Let's utilize the spaces by making all blocks on the same row.
We'll do this by resetting the
width
of.footer-heading
back to itsinitial
.In addition, we'll align
.footer-heading
to thecenter
usingalign-self
. And nudge it up with a negative margin so it aligns to the nearest text line of the "About Us" section.Also, I changed the
padding
of.page-footer
. Personal preference.And all these rules will take effect when the minimum screen width is 900px.
@media screen and (min-width: 900px) { .footer-heading { width: initial; align-self: center; margin-top: 0.4rem; } .page-footer { padding: 2rem 1.5rem 1.5rem; } }
-
Design for Extra Large Screens
At some point, still, the space will become too wide.
But there're no elements to fill in.
Hence, let's restrict the space from growing further.
One trick is to insert empty elements at both ends with a width of 2%. They will act like normal blocks so that the space created by
justify-content: space-around;
are minimized.If we know the width of the main content, we can use other methods to make sure they align together at both edges.
/* For Extra-Large Screens - Limit the space growth between blocks */ @media screen and (min-width: 1440px) { .page-footer::before, .page-footer::after { content: ''; width: 2%; } }
This steps completes the stage on creating Responsive Designs
That's it! We are done. We created an accessible webpage from a mockup.
Below is the whole code on Codepen. You can view it on a new tab and experience its responsiveness.
See the Pen UI to Html, CSS by Toheeb Ogunbiyi (@toheeb) on CodePen.
Feel free to compare it to that of the live website
On how readable the markup is. Which means any designer can jump on it and understand what's is going on
Or how accessible it is to Screen Readers, Keyboard, Touch or Mouse Users.
Or how better it looks. Oh yes!
Now, over to you.
What's the number one thing you learnt from this post?
Are you going to start making your website designs accessible to everyone?
Or maybe you've got a question about something.
Either way, let me know on Twitter @toheebdotcom