The past few lessons covered how to build layouts that work across screen sizes and devices. This week is about making sure those layouts work for everyone: including people who navigate with a keyboard, use a screen reader, have low vision, or have difficulty distinguishing certain colors.
Accessibility often gets treated as an optional polish step, something you revisit after the "real" work is done. That framing is both wrong and costly. Adding accessibility into a finished product is significantly harder than building it in from the start. The habits you form this week will make every project you build more robust, and more professional!
This lesson focuses on the parts of accessibility that live in CSS and HTML: focus states, color contrast, typography, spacing, and motion. You won't need a library or a framework. Most of what makes a UI accessible is just writing the right CSS and not removing what the browser gives you for free.
A note on scope: full accessibility covers a lot of ground — ARIA roles, semantic HTML, screen reader behavior, form labeling. This lesson focuses specifically on the styling side. Semantic HTML and ARIA are covered in the HTML module; what you learn today builds directly on top of that foundation.
An accessible interface isn't a special version of your UI for a small group of users. It's the same UI, working correctly for a wider range of people and situations.
Consider the range of people who might visit something you build: someone navigating with a keyboard because their mouse broke. A user with low vision who has increased their system font size to 20px. A person with color blindness who can't distinguish your red error state from your green success state. Someone on a slow connection using a screen reader on a phone. A user who gets motion sickness from animated page transitions.
None of these are edge cases. The WHO estimates over a billion people live with some form of disability. Color blindness affects roughly 8% of men. Keyboard navigation is used by people with motor disabilities, power users, and anyone whose trackpad just stopped working.
Beyond the ethical argument, there's a practical one: in many countries, public-facing websites are legally required to meet accessibility standards. The Web Content Accessibility Guidelines (WCAG) are the international standard, and court cases around inaccessible websites have increased steadily.
Browsers ship with built-in accessibility features: focusable elements, keyboard navigation, visible focus rings, default button behavior, form label associations. The most common source of accessibility failures in CSS is removing these defaults without replacing them:
/* ❌ This one rule breaks keyboard navigation for millions of users */
* { outline: none; }
/* ❌ Same result, just written differently */
button:focus { outline: 0; }
If you've ever added outline: none to remove the "ugly blue ring" on focused elements, this section is for you.
When a user navigates a page with a keyboard (using Tab to move forward, Shift+Tab to move backward, Enter/Space to activate), the browser tracks which element is currently active. That element is "focused." The visible indicator showing which element is focused is called the focus indicator or focus ring.
Without a visible focus indicator, keyboard users are navigating blind: they have no way to tell where they are on the page.
/* What most browsers show by default */
button:focus {
outline: 2px solid #0078d4; /* a colored ring around the element */
outline-offset: 2px; /* gap between the ring and the element */
}
The browser's default focus ring is a blue outline that looks slightly different in every browser. Designers often remove it because it clashes with the visual design or looks unpolished. This is understandable, because the default ring genuinely isn't beautiful…
The fix is to replace it, not remove it:
/* ❌ Removes focus visibility for keyboard users */
:focus {
outline: none;
}
/* ✅ Replaces it with something that fits the design */
:focus {
outline: 2px solid #6366f1;
outline-offset: 3px;
}
Any visible indicator works: a ring, a box shadow, a background color change, an underline — as long as it clearly marks the focused element. The requirement is that it's visible, not that it uses outline.
The reason focus rings feel intrusive in most designs is that they appear even when using a mouse. Click a button with your mouse, and the blue ring stays visible. That ring exists for keyboard users, not mouse users: it's just that CSS historically couldn't tell the difference.
:focus-visible solves this. It only applies focus styles when the browser determines the focus indicator would be useful, which is almost always keyboard navigation:
/* Ring only shows for keyboard navigation, not mouse clicks */
button:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 3px;
}
Mouse users click a button, no ring. Keyboard users Tab to a button, ring appears. Both groups get an appropriate experience.
Browser support for :focus-visible is now excellent across Chrome, Firefox, and Safari. Use it in new projects without hesitation.
/* A clean pattern: remove default outline, add your own via :focus-visible */
:focus {
outline: none; /* safe here because we're replacing it below */
}
:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 3px;
border-radius: 4px; /* matches the element's shape */
}
<aside> ⚠️
Warning
The :focus + :focus-visible pattern above is fine in modern browsers. But if you only write :focus-visible and omit :focus, some older browsers that don't support :focus-visible will fall back to showing their default outline. The safest approach is to set :focus { outline: none } alongside a custom :focus-visible rule, so all browsers get some focus styling.
</aside>
A focus indicator needs to be visually distinct from the element's default state. WCAG 2.2 introduced formal requirements for focus indicators: at minimum, the focus ring should have a 3:1 contrast ratio against the adjacent colors and be at least 2px thick.
In practice:
/* ✅ High contrast ring — works on light and dark backgrounds */
:focus-visible {
outline: 3px solid #fbbf24; /* yellow rings are highly visible */
outline-offset: 2px;
}
/* ✅ Using box-shadow when outline would clip */
.card:focus-visible {
outline: none;
box-shadow: 0 0 0 3px #6366f1; /* ring that respects border-radius */
}
/* ✅ Inset ring for elements where outward ring would overlap siblings */
.tab:focus-visible {
outline: none;
box-shadow: inset 0 0 0 3px #6366f1;
}
The box-shadow approach is useful when outline clips or overlaps in tight layouts. box-shadow respects border-radius; outline in older browsers doesn't (though modern browsers have fixed this).
<aside> ⚠️
Info
Yellow and white focus rings are popular in design systems because they work against both light and dark backgrounds. GitHub, GOV.UK, and many large design systems use a high-contrast yellow (#ffbf47 or similar) for precisely this reason.
</aside>
Some components need custom focus management in JavaScript: modals that trap focus, dropdowns that move focus to the first item, skip-navigation links. That's covered in the JavaScript module. The CSS groundwork you're laying here is what makes those patterns work visually.
One CSS pattern worth knowing now: skip links. A skip link lets keyboard users jump past the navigation directly to the main content:
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content">...</main>
.skip-link {
position: absolute;
top: -100%; /* hidden above the viewport */
left: 1rem;
padding: 0.5rem 1rem;
background: #1e293b;
color: white;
border-radius: 0 0 4px 4px;
text-decoration: none;
font-weight: 600;
z-index: 100;
}
/* Slides into view when focused via keyboard */
.skip-link:focus {
top: 0;
}
The first Tab keypress on any page with this pattern will reveal the skip link. Screen reader users and keyboard users can jump to the content immediately without tabbing through every navigation link. This is a small CSS feature with a large usability impact.
<aside> ⌨️
Hands On
Open any website — your own or a popular one. Press Tab repeatedly and watch what happens. Does a focus ring appear? Does it disappear? Do you ever lose track of where you are on the page? Now open your most recent project file and press Tab through it. Add :focus-visible styles to your buttons and links if they're missing.
</aside>
Color contrast is the difference in luminance between two colors, typically text and its background. Low contrast makes text hard to read for everyone, and unreadable for users with low vision or color blindness.
WCAG defines contrast requirements as ratios. A ratio of 1:1 means identical colors (invisible text). A ratio of 21:1 is black on white (maximum possible contrast).
The WCAG requirements:
| Text size | Minimum (AA) | Enhanced (AAA) |
|---|---|---|
| Normal text (under 18px or 14px bold) | 4.5:1 | 7:1 |
| Large text (18px+ or 14px+ bold) | 3:1 | 4.5:1 |
| UI components, icons | 3:1 | — |
AA is the standard legal and ethical baseline. AAA is best-effort for text-heavy content.
Don't guess at contrast ratios, but measure them! A color that looks fine to you may fail for someone with low vision.
In DevTools: Chrome and Firefox both have a contrast checker built into the color picker. Open DevTools → Styles panel → click any color value → a contrast ratio appears with a pass/fail indicator.
Online: WebAIM Contrast Checker lets you enter hex values and see the ratio. Coolors Contrast Checker is similarly useful.
In your workflow: The browser extension axe DevTools will audit an entire page and flag all contrast failures at once.
/* ❌ Fails WCAG AA — light grey on white is ~2.3:1 */
.helper-text {
color: #aaaaaa;
background: #ffffff;
}
/* ✅ Passes WCAG AA — darker grey on white is ~7:1 */
.helper-text {
color: #595959;
background: #ffffff;
}
Gray text on white backgrounds. Light grays are a persistent offender: they look clean and subtle in a design, but often fail AA. Text that carries information (labels, descriptions, error messages) needs sufficient contrast.
/* ❌ Common failure: "subtle" label text */
label { color: #999; }
/* ✅ Still looks secondary, but passes */
label { color: #6b7280; } /* ~4.6:1 on white — just passes AA */
Colored text on colored backgrounds. Two saturated colors can look high-contrast to the eye but fail the ratio test, especially certain red/green combinations that affect colorblind users.
/* ❌ Red on dark red — visually close, fails badly */
.badge-error {
color: #ef4444;
background: #991b1b;
}
/* ✅ White on dark red — clearly legible */
.badge-error {
color: #ffffff;
background: #991b1b; /* white on this red is ~5.9:1 */
}
Placeholder text. Browser default placeholder text is notoriously low-contrast. If you style it, keep it readable:
/* ❌ Too light */
::placeholder { color: #d1d5db; }
/* ✅ Passes AA */
::placeholder { color: #9ca3af; }
This is distinct from contrast but closely related. When you use color to convey meaning, users who can't distinguish those colors are locked out of that information.
/* ❌ Only color distinguishes valid from invalid */
.input-valid { border-color: green; }
.input-invalid { border-color: red; }
/* ✅ Color + icon + text all reinforce the state */
.input-invalid {
border-color: #dc2626;
/* pair with an icon and error message text in the HTML */
}
For form validation, error states, status badges, and data visualizations: always pair color with at least one other signal: text, an icon, a pattern, or a shape change.
<aside> 💡
</aside>
<aside> ⌨️
</aside>