Modern CSS (2025–2026)Extra· 40 min read

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.

:focus-visible shows a ring for keyboard users
<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>
Live preview

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.

An animation that politely disables itself
<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>
Live preview

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:

Dark mode by swapping variable values
<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>
Live preview

The trick is that the colours are never hard-coded onto elements — they all read from variables. Here is the flow, in order:

  1. Define your theme colours once as variables on :root: --bg, --text, --card, --brand.
  2. Style every element with var(--bg), var(--text) and so on — never a raw HEX.
  3. Add @media (prefers-color-scheme: dark) and inside it redefine the same variables with dark values.
  4. 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?

Answer: :focus-visible shows the focus indicator only when it is genuinely useful (keyboard navigation), keeping the design clean for mouse users while preserving accessibility.

✍️ Practice

  1. Give every button and form field a clear :focus-visible outline so keyboard users can navigate your page.
  2. Add a prefers-color-scheme: dark block that swaps your colour variables, and test it by changing your system theme.

🏠 Homework

  1. 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.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →