Responsive Styling and Accessibility: Mobile First CSS, Focus, Contrast and Dark Mode

MERN Stack Full Course: MongoDB, Express, React and Node.js · Connecting Everything · 7 min read · Updated

One layout, every screen

  1. Mobile first: write the styles for a small screen first, with one column.
  2. Add min-width media queries to use extra space on bigger screens.
  3. Grid and flexible sizes let cards fill the space without fixed widths.
  4. Accessibility is part of the same work: visible focus rings, labels, enough colour contrast and keyboard use.
  5. prefers-color-scheme adds a dark mode that follows the visitor's device. Test with the keyboard only.

Many of the people using a job tracker use it on a phone, often on a slow connection, sometimes with large text or a screen reader, and sometimes in a dim room. Design for them first and the app is better for everyone. This lesson replaces the basic styles with a responsive, accessible stylesheet and adds three small changes to the layout.

Mobile first

/* Lesson 34: responsive, accessible styling. Mobile first: the base rules are for phones, @media adds room on bigger screens. */
:root {
  --bg:#f8fafc; --surface:#fff; --ink:#0f172a; --muted:#52617a; --line:#e2e8f0;
  --brand:#4338ca; --brand-ink:#fff; --danger:#b91c1c; --ok:#166534; --radius:12px;
}
@media (prefers-color-scheme: dark) {
  :root { --bg:#0b1120; --surface:#111a2e; --ink:#e5e9f2; --muted:#a3b0c7; --line:#25324d; --brand:#818cf8; --brand-ink:#0b1120; --danger:#fca5a5; --ok:#86efac; }
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin:0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background:var(--bg); color:var(--ink); line-height:1.55; }
h1 { font-size:1.6rem; line-height:1.2; margin:.5em 0 .6em; }
a { color:var(--brand); }
main:focus { outline:none; }
:focus-visible { outline:3px solid var(--brand); outline-offset:2px; border-radius:4px; }

.skip-link { position:absolute; inset-inline-start:8px; top:-48px; background:var(--brand); color:var(--brand-ink); padding:8px 12px; border-radius:8px; z-index:10; }
.skip-link:focus { top:8px; }

.container { max-width: 900px; margin: 0 auto; padding: 16px; }
.site-header { display:flex; gap:12px 16px; align-items:center; flex-wrap:wrap; padding:10px 16px; background:var(--surface); border-bottom:1px solid var(--line); }
.nav { display:flex; gap:6px; flex-wrap:wrap; }
.nav a { padding:6px 12px; border-radius:999px; text-decoration:none; color:var(--muted); }
.nav a.active { background:color-mix(in srgb, var(--brand) 14%, transparent); color:var(--brand); font-weight:600; }
.push { margin-inline-start:auto; }
.muted { color:var(--muted); font-size:.92rem; }

.card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin:12px 0; }
.card h2 { margin:0 0 4px; font-size:1.1rem; }
.narrow { max-width:440px; margin-inline:auto; }

.badge { display:inline-block; padding:2px 10px; border-radius:999px; font-size:.8rem; font-weight:600; background:#e2e8f0; color:#1e293b; }
.badge-applied { background:#dbeafe; color:#1e3a8a; }
.badge-interview { background:#fef3c7; color:#78350f; }
.badge-offer { background:#dcfce7; color:#14532d; }
.badge-rejected { background:#fee2e2; color:#7f1d1d; }
.row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px; }

select, input, textarea, button { font:inherit; color:inherit; }
.form { display:grid; gap:12px; grid-template-columns: 1fr; }
.field { display:grid; gap:4px; min-width:0; }
.field label { font-size:.9rem; font-weight:600; }
.wide { grid-column: 1 / -1; }
.field input, .field select, .field textarea { padding:10px 12px; min-height:44px; border:1px solid #94a3b8; border-radius:8px; background:var(--surface); width:100%; }
.field input[aria-invalid="true"] { border-color:var(--danger); border-width:2px; }
.error { color:var(--danger); font-size:.9rem; margin:4px 0 0; }
.ok { color:var(--ok); }

.btn { padding:10px 18px; min-height:44px; border:0; border-radius:8px; background:var(--brand); color:var(--brand-ink); font-weight:600; cursor:pointer; }
.btn:hover { filter:brightness(1.1); }
.btn:disabled { opacity:.55; cursor:not-allowed; }
.btn.secondary { background:var(--surface); color:var(--ink); border:1px solid #94a3b8; }
.link-btn { background:none; border:0; padding:4px 2px; color:var(--brand); text-decoration:underline; cursor:pointer; min-height:32px; }
.link-btn.danger { color:var(--danger); }

.stats { display:grid; grid-template-columns: repeat(2, 1fr); gap:8px; margin:12px 0; }
.stat { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:8px 12px; display:grid; }
.stat strong { font-size:1.4rem; }
.stat span { color:var(--muted); font-size:.8rem; }
.filters { display:grid; gap:10px; grid-template-columns: 1fr; margin:12px 0; }
.pager { display:flex; gap:12px; align-items:center; justify-content:center; margin:16px 0; flex-wrap:wrap; }
details summary { cursor:pointer; font-weight:600; min-height:32px; }

@media (min-width: 600px) {
  .form { grid-template-columns: repeat(2, 1fr); }
  .stats { grid-template-columns: repeat(4, 1fr); }
  .filters { grid-template-columns: 2fr 1fr 1fr; }
}
@media (min-width: 900px) {
  .stats { grid-template-columns: repeat(7, 1fr); }
}
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; scroll-behavior: auto !important; } }

Mobile first means the base rules are written for a narrow screen, and @media (min-width: 600px) blocks *add* room for bigger ones. The form is one column on a phone and two on a tablet. The statistics are two columns, then four, then seven. This is easier than the other way round, and phones get the least CSS to process.

  • Flexible units and layout. grid with 1fr columns, flex-wrap, max-width and no fixed widths mean nothing sticks out of a 390 px screen. Test by opening the browser tools and choosing a phone size. The page must never scroll sideways.
  • Touch targets. Buttons and inputs have min-height: 44px, because fingers are less precise than a mouse pointer.
  • Logical properties such as margin-inline-start and inset-inline-start flip automatically for right-to-left languages like Arabic, so the same CSS serves them.

Colour, contrast and dark mode

Text needs enough contrast against its background to be read in sunlight and by people with low vision. The muted text colour was darkened from the first version for that reason, and the status badges pair dark text with a light background. A quick check is to use the contrast checker in the browser tools. The prefers-color-scheme: dark block swaps the custom properties when the device is set to dark mode, so the whole interface follows with a few lines, because every colour comes from a variable.

Focus and keyboard use

  • :focus-visible draws a clear outline for keyboard users and stays out of the way for mouse users. Never remove the outline without replacing it.
  • A skip link (“Skip to content”) is the first thing a keyboard user can reach. It is hidden above the page until it gets focus. It lets them jump past the header on every page.
  • After a page change in a single page app, nothing tells a screen reader that the page changed. The layout now moves focus to `<main>` when the address changes, so the new content is read out.
import { useEffect, useRef } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import Header from './Header.jsx';

export default function Layout() {
  const main = useRef(null);
  const { pathname } = useLocation();

  // After a page change, move keyboard and screen reader focus to the new content (a single-page app does not reload).
  useEffect(() => { main.current?.focus({ preventScroll: true }); }, [pathname]);

  return (
    <>
      <a className="skip-link" href="#main">Skip to content</a>
      <Header />
      <main id="main" tabIndex={-1} ref={main} className="container">
        <Outlet />
      </main>
    </>
  );
}

tabIndex={-1} makes main focusable by code without adding it to the Tab order, and focus({ preventScroll: true }) avoids the page jumping. Together with id="main" they give the skip link a target.

Respect the person's settings

The last rule in the stylesheet switches off animations and transitions when the device asks for reduced motion. Some people feel dizzy or sick from movement on screen. The same attention applies to text size: use rem or relative sizes so the browser's font-size setting works.

A quick accessibility test you can do today

  1. Put the mouse away and do the whole flow with Tab, Shift+Tab, Enter and the arrow keys.
  2. Zoom the page to 200 percent. Nothing should overlap or be cut off.
  3. Turn on a screen reader (VoiceOver on macOS and iOS, TalkBack on Android, NVDA on Windows) and listen to one page.
  4. Run the Lighthouse accessibility audit in Chrome and fix what it reports.

Your task

  1. Copy the new stylesheet and layout, then check the app at 390 px, 768 px and a wide screen.
  2. Switch your computer to dark mode and reload.
  3. Tab through the home page. Is there a visible focus outline at every step?
  4. Fix any text that fails the contrast checker in the browser tools.

How to check it worked

  • No sideways scrolling at 390 px.
  • A visible focus ring on every interactive element.
  • Dark mode and reduced motion are respected.

Summary

  • Write CSS for phones first and add min-width media queries for larger screens.
  • Keep contrast high, never rely on colour alone, and keep focus outlines.
  • Add a skip link, move focus on page change, and respect reduced motion and dark mode.

Download the solution (mern-lesson-34.zip)

Frequently asked questions

What does mobile first mean?

You write the default styles for a small screen and use min-width media queries to add layout for larger ones. The smallest, simplest layout is the base, and bigger screens add to it.

How big should buttons be on a phone?

Aim for about 44 by 44 CSS pixels, which is a widely used guideline for touch targets, so people can tap them without hitting the wrong thing.

Why move focus to main after a page change?

In a single page app the browser does not reload, so screen readers get no signal that a new page appeared. Moving focus to the main content announces it and puts keyboard users in the right place.