Accessible CSS & Dark Mode
Professional CSS respects the user: visible keyboard focus, motion that can be turned off, and a dark theme that follows the system. These are baseline expectations on the job.
What you will learn
- Add a clear keyboard focus style with :focus-visible
- Respect prefers-reduced-motion
- Build a dark mode with prefers-color-scheme and variables
Keyboard focus with :focus-visible
Some people navigate with the keyboard (pressing Tab), not a mouse. They rely on a visible focus ring to see which element is selected. The old :focus rule showed that ring for mouse clicks too, which looked noisy, so developers often removed it — and accidentally broke keyboard access.
:focus-visible is the modern fix: the browser shows the ring only when it is genuinely helpful — typically for keyboard users — and hides it for ordinary mouse clicks. So you get a clean look and full accessibility.
<style>
.btn { background:#4338ca; color:#fff; border:none; padding:12px 22px; border-radius:10px; cursor:pointer; }
/* A strong, visible ring — but only when focus should be shown (keyboard) */
.btn:focus-visible { outline: 3px solid #f59e0b; outline-offset: 3px; }
</style>
<button class="btn">Press Tab to focus me</button>
<button class="btn">Then Tab to me</button>Click the buttons with your mouse and you see no harsh ring; press Tab to move between them with the keyboard and a clear amber outline appears. The rule .btn:focus-visible applies the ring only when the browser decides focus should be visible (keyboard navigation), and outline-offset: 3px pushes the ring slightly away so it does not touch the button. This gives you the best of both worlds — tidy for mouse, accessible for keyboard.
Watch out: Never write outline: none on its own. If you remove the default focus ring, you must provide a visible :focus-visible style, or keyboard users cannot see where they are. This is a hard accessibility requirement, not a preference.
Respecting reduced motion
Animations can cause nausea or discomfort for people with vestibular conditions. Operating systems let users ask for reduced motion, and CSS can read that preference with a media query: @media (prefers-reduced-motion: reduce). Inside it, you switch off or shorten your animations.
<style>
@keyframes slide { from { transform: translateX(-30px); opacity: 0; } to { transform: none; opacity: 1; } }
.banner { animation: slide .6s ease both; background:#eef2ff; padding:16px; border-radius:10px; }
/* If the user prefers less motion, turn the animation off */
@media (prefers-reduced-motion: reduce) {
.banner { animation: none; }
}
</style>
<div class="banner">I slide in — unless your system asks for reduced motion.</div>The .banner normally slides in with a keyframe animation. The @media (prefers-reduced-motion: reduce) block listens for the user’s system setting; if they have asked for less motion, animation: none cancels the slide and the banner simply appears. You did not have to ask the user anything — the browser relays their OS preference, and your CSS honours it.
Dark mode that follows the system
Most devices have a light/dark setting. CSS can detect it with @media (prefers-color-scheme: dark). The cleanest way to support both themes is to put your colours in CSS variables (from the Variables lesson) and simply swap the variable values in the dark block — every element that uses the variables updates at once. Here is the worked pattern:
<style>
:root {
--bg: #ffffff; --text: #1f2333; --card: #f3f4f8; --brand: #4338ca;
}
/* When the OS is in dark mode, redefine the SAME variables */
@media (prefers-color-scheme: dark) {
:root { --bg: #14172a; --text: #e6e8f5; --card: #1f2540; --brand: #8b9cff; }
}
.demo { background: var(--bg); color: var(--text); padding: 20px; border-radius: 12px; }
.demo .card { background: var(--card); padding: 14px; border-radius: 10px; margin-top: 10px; }
.demo h4 { color: var(--brand); margin: 0 0 6px; }
</style>
<div class="demo">
<h4>Auto theme</h4>
<p>This block reads your system light/dark setting.</p>
<div class="card">A card that recolours in dark mode.</div>
</div>The trick is that the colours are never hard-coded onto elements — they all read from variables. Here is the flow, in order:
- Define your theme colours once as variables on
:root:--bg,--text,--card,--brand. - Style every element with
var(--bg),var(--text)and so on — never a raw HEX. - Add
@media (prefers-color-scheme: dark)and inside it redefine the same variables with dark values. - When the OS switches to dark mode, the variables flip to their dark values, and because every element reads those variables, the whole UI recolours instantly — no element rules change.
Note: If your device is set to light mode you will see the light theme; switch your system to dark and the same block recolours automatically. This is exactly how production sites offer light/dark themes from a single stylesheet.
Tip: Because CSS variables are live, you can also add a manual toggle: a button (with a little JavaScript) that sets the dark variables on a data-theme="dark" attribute — letting users override the system setting. The variable approach powers both.
Q. Why is :focus-visible preferred over removing focus outlines?
✍️ Practice
- Give every button and form field a clear
:focus-visibleoutline so keyboard users can navigate your page. - Add a
prefers-color-scheme: darkblock that swaps your colour variables, and test it by changing your system theme.
🏠 Homework
- Make one of your projects fully accessible: visible focus styles everywhere, animations disabled under
prefers-reduced-motion, and a system-driven dark mode via variables.