Make it responsive, accessible and fast
Test the Brew & Bean site at phone width, fix the layout with flexible grids and media queries, add keyboard and screen reader support, set page titles, and measure speed with a production build and Lighthouse. Every fix is a small change you can check in the browser.
The site works on your laptop. Now make it work for everyone: on a small phone screen, with a keyboard, with a screen reader, and on a slow mobile connection. These three things (responsive, accessible, fast) are what separate a demo from a site you can show a client or an interviewer.
You will test the current site, find real problems, fix them with small changes and measure again. AI helps by suggesting problems and fixes, but you check every one in the browser.
Start from: the project at the end of *Add a cart and order form*, committed. Use Chrome or Edge for the DevTools steps below (Firefox has similar tools).
Step 1: Test on a phone-sized screen
Most of Brew & Bean's customers will order from a phone, so test there first. You don't need a phone: browser DevTools can pretend to be one.
Test at 360 px
- 1Open the site in Chrome or Edge and press F12 to open DevTools.
- 2Click the device toolbar icon (a phone and tablet) or press Ctrl+Shift+M.
- 3Choose a small device such as "iPhone SE" or set the width to 360 by hand. 360 px is a common small Android width.
- 4Visit Home, Menu, Cart and the order form. Add two items to the cart first so the cart rows show.
- 5Write down everything that looks wrong: text that is cut off, sideways scrolling, buttons too close together.
You will probably find these problems in the first version of the CSS:
- The menu grid still has three columns, so the cards are squeezed. - The cart rows have four columns and the text overlaps. - The hero heading is huge and the two buttons are side by side and cramped. - The cart and the form sit side by side instead of one under the other. - The buttons are small for a thumb.
This is normal. AI-written CSS often looks right on the screen of whoever tested it. Always check at 360 px.
Here is the CSS and a description of a React page. Act as a front-end reviewer. I am testing at 360 px wide and the page looks wrong.
Problems I see:
- [describe, for example: menu cards are squeezed, cart rows overlap]
<css>
[paste src/index.css]
</css>
List the CSS rules that cause each problem and give the smallest fix for each (a changed rule or a media query). Prefer CSS grid with auto-fill and minmax over fixed column counts. Do not add a CSS framework.What to expect: Check that each fix names the exact rule, that it uses a media query or a flexible grid (not fixed pixel widths), and that you can see the result in DevTools.
Step 2: Make the layout responsive
Responsive design means the same page adapts to the space it has. There are two tools.
Flexible grids. Instead of "always 3 columns", say "as many columns as fit, each at least 220 px wide": repeat(auto-fill, minmax(220px, 1fr)). On a phone you get one column, on a laptop three or four. No media query needed.
Media queries. For changes a flexible grid cannot do (stacking the cart and the form, rearranging a row), use @media (max-width: 600px) { ... }. The rules inside apply only when the screen is 600 px wide or less.
First check the viewport tag is in index.html. It is already there from Vite: <meta name="viewport" content="width=device-width, initial-scale=1.0">. Without it phones pretend to be wide and shrink the page.
Make three edits to the existing rules in src/index.css. Find each rule and change only the line shown.
/* 1. .info-grid: replace the grid-template-columns line */
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
/* 2. .menu-grid: replace the grid-template-columns line */
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
/* 3. .hero h1: replace the font-size line */
font-size: clamp(2rem, 6vw, 3rem);clamp(min, preferred, max) lets the heading shrink smoothly on small screens and never grow beyond 3rem.
Then add the block below at the end of the file. It does four things: it adds helper classes for accessibility (used in step 3), it makes buttons big enough to tap, it stacks the cart and form under 800 px, and it re-arranges the header, hero and cart rows under 600 px. The cart row becomes a two-line card: name and price on the first line, quantity and Remove on the second.
/* Accessibility helpers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: absolute;
left: 16px;
top: -100px;
z-index: 10;
padding: 8px 16px;
background: var(--brand);
color: #ffffff;
border-radius: 0 0 8px 8px;
}
.skip-link:focus {
top: 0;
}
:focus-visible {
outline: 3px solid var(--brand);
outline-offset: 2px;
}
main:focus {
outline: none;
}
.btn:focus-visible {
outline-color: var(--text);
}
/* Bigger tap targets for touch screens */
.btn,
.chip {
min-height: 44px;
}
.qty-btn {
width: 44px;
height: 44px;
}
.link-button {
padding: 8px 4px;
}
/* Tablets and phones */
@media (max-width: 800px) {
.cart-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.container {
padding: 0 16px;
}
.header-inner {
flex-direction: column;
gap: 8px;
}
.hero {
padding: 40px 0;
}
.hero-actions {
flex-direction: column;
}
.hero-actions .btn {
text-align: center;
}
.cart-line {
grid-template-columns: 1fr auto;
grid-template-areas:
'name price'
'qty remove';
gap: 8px 16px;
}
.cart-line-name {
grid-area: name;
}
.cart-line-price {
grid-area: price;
text-align: right;
}
.qty {
grid-area: qty;
}
.cart-line .link-button {
grid-area: remove;
justify-self: end;
}
.confirmation {
padding: 20px;
}
}A tap target of about 44 by 44 px is a widely used guideline for touch screens.
Finding sideways scroll
If a page scrolls sideways at 360 px, something is wider than the screen. In DevTools, right-click the page, choose Inspect, and hover over elements in the Elements panel to find the one that sticks out. Long words, wide tables and fixed pixel widths are the usual causes.
Step 3: Make it accessible
Accessibility means people can use the site with a keyboard, a screen reader, zoomed text or poor eyesight. It also helps everyone else: good labels and focus styles make a site pleasant for all. In India, many users are on small screens in bright sunlight, where contrast and large buttons matter a lot.
You already did a lot of this in earlier lessons: real buttons and links, labels on every field, aria-pressed on the filters, error messages linked to fields, and aria-hidden on decoration. Now we add the rest.
Accessibility fixes
| Problem | Fix in this lesson |
|---|---|
| Keyboard users must tab through the header on every page | A "Skip to content" link at the top |
| No clear outline when you tab to a button | A :focus-visible outline in the CSS |
| Screen readers hear "Cart 3" with no meaning | Hidden text "(3 items)" and a live status message |
| After an invalid submit, focus stays on the button | Move focus to the first invalid field |
| After placing an order, nothing tells a screen reader user | Move focus to the confirmation heading |
| Every page has the same title in the browser tab | A small hook that sets a title per page |
Skip link, lazy pages and the main area. App.jsx gets a skip link as the first focusable element, tabIndex={-1} on main so the link can move focus there, and a Suspense wrapper for the lazy pages we add in step 4.
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
import Header from './components/Header.jsx'
import Footer from './components/Footer.jsx'
import Home from './pages/Home.jsx'
import NotFound from './pages/NotFound.jsx'
// These pages load only when someone visits them.
const Menu = lazy(() => import('./pages/Menu.jsx'))
const Cart = lazy(() => import('./pages/Cart.jsx'))
export default function App() {
return (
<div className="app">
<a href="#main" className="skip-link">
Skip to content
</a>
<Header />
<main id="main" tabIndex={-1}>
<Suspense
fallback={
<p className="container page" role="status">
Loading...
</p>
}
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/menu" element={<Menu />} />
<Route path="/cart" element={<Cart />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</main>
<Footer />
</div>
)
}We build on the last version: skip link, tabIndex on main, and lazy loaded Menu and Cart pages inside Suspense.
Header. The visible badge is hidden from screen readers (aria-hidden) and replaced with hidden text that reads "(3 items)". A small role="status" message announces changes to the cart politely. The sr-only class hides text visually but keeps it for screen readers.
import { Link, NavLink } from 'react-router-dom'
import { useCart } from '../context/CartContext.jsx'
export default function Header() {
const { totalItems } = useCart()
return (
<header className="site-header">
<div className="container header-inner">
<Link to="/" className="brand">
Brew & Bean
</Link>
<nav aria-label="Main">
<ul className="nav-list">
<li>
<NavLink to="/" end>
Home
</NavLink>
</li>
<li>
<NavLink to="/menu">Menu</NavLink>
</li>
<li>
<NavLink to="/cart">
Cart
{totalItems > 0 && (
<>
<span className="cart-badge" aria-hidden="true">
{totalItems}
</span>
<span className="sr-only"> ({totalItems} items)</span>
</>
)}
</NavLink>
</li>
</ul>
</nav>
<p className="sr-only" role="status">
{totalItems === 0 ? 'Cart is empty' : `${totalItems} items in cart`}
</p>
</div>
</header>
)
}Page titles. In a single-page app the browser tab title never changes unless you change it. Create this tiny hook:
import { useEffect } from 'react'
const SITE_NAME = 'Brew & Bean'
// Sets the browser tab title. Call it with no argument on the home page.
export default function useDocumentTitle(title) {
useEffect(() => {
document.title = title
? `${title} | ${SITE_NAME}`
: `${SITE_NAME} | Coffee shop in Indiranagar, Bengaluru`
}, [title])
}Called with no argument it sets the home title. The file lives in a new folder: src/hooks.
Then call it at the top of each page component. Add the import line at the top of the file and the call as the first line inside the function:
// src/pages/Home.jsx
import useDocumentTitle from '../hooks/useDocumentTitle.js'
export default function Home() {
useDocumentTitle()
// ...the rest stays the same
}
// src/pages/Menu.jsx
// useDocumentTitle('Menu')
// src/pages/Cart.jsx
// useDocumentTitle('Your cart')
// src/pages/NotFound.jsx
// useDocumentTitle('Page not found')Each page imports the hook with the same import line; only the argument changes. Keep the other lines as they are.
Move focus to the first error. In OrderForm.jsx, inside handleSubmit, replace the line if (Object.keys(found).length > 0) return with this block. Without it, a keyboard or screen reader user presses the button and nothing seems to happen.
const firstInvalid = ['name', 'phone'].find((field) => found[field])
if (firstInvalid) {
document.getElementById(firstInvalid).focus()
return
}The field ids are name and phone, so getElementById finds the input.
Move focus to the confirmation. In OrderConfirmation.jsx add useEffect and useRef to the import, add the ref and effect at the top of the component, and attach the ref to the heading. A screen reader then announces the heading when the order is placed.
import { useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { formatPrice } from '../utils/format.js'
export default function OrderConfirmation({ order }) {
const headingRef = useRef(null)
// Move focus to the heading so screen readers announce the confirmation.
useEffect(() => {
headingRef.current.focus()
}, [])
return (
<section className="container page">
<div className="confirmation">
<h1 ref={headingRef} tabIndex={-1}>Thank you, {order.name}!</h1>
{/* ...the rest of the component stays the same */}tabIndex={-1} lets code focus the heading, but keeps it out of the normal Tab order.
Colour contrast. Text must be easy to read against its background. The common standard (WCAG AA) asks for a contrast ratio of at least 4.5 to 1 for normal text. Our palette was chosen with this in mind: the dark brown text on the cream background is above 15 to 1, the grey-brown secondary text is about 6 to 1, and white on the coffee brown buttons is above 7 to 1. If you change colours, check them again with a contrast checker (the Chrome DevTools colour picker shows the ratio).
Step 4: Make it fast
Speed matters most on slow phones and mobile data, which is how many people in India will open your site. Our site is already small because we made some good choices: no UI library, no images yet, system fonts (no font files to download), and plain CSS.
The main tool left is code splitting: send the code for a page only when someone visits it. React.lazy and import() do this; we already added them in App.jsx. Let's measure what it did.
Measure
- 1Stop the dev server. Run
npm run buildin the terminal. - 2Look at the files listed under dist/assets. You should see a separate small file for each lazy page (for example Cart-xxxx.js) next to the main index file.
- 3Run
npm run preview. This serves the built site (open the address it prints). It is a better place to measure speed than the dev server, which is not optimised. - 4In Chrome DevTools open the Lighthouse tab, choose Mobile and run the Performance, Accessibility and Best Practices checks on the Home page.
- 5Read the report. Fix what it lists for accessibility first; those fixes are usually quick.
dist/index.html 0.72 kB
dist/assets/index-xxxx.css about 6 kB
dist/assets/Menu-xxxx.js under 1 kB
dist/assets/Cart-xxxx.js about 5 kB
dist/assets/index-xxxx.js about 266 kB (React and the router)Notice what this tells you: almost all the weight is React and the router, which every visitor needs. The lazy Cart page saves only a few kilobytes, and Menu saves almost nothing because it shares its card with Home. Measuring shows you what really matters. For a site this small, the biggest speed gains come later from images, if you add real photos. When you do:
- Resize photos to the size they are shown at. Don't upload a 4000 px photo for a 300 px card.
- Use modern formats such as WebP or AVIF where you can.
- Give every <img> its width and height so the page does not jump while loading.
- Add loading="lazy" to images that are below the first screen.
- Write real alt text for photos that carry meaning, and alt="" for decoration.
- Only use photos you took or have a licence for.
<img
src="/images/cappuccino.webp"
alt="A cappuccino with latte art in a white cup"
width="300"
height="200"
loading="lazy"
/>Example only. We have no photos yet, so don't add this to the project unless you have your own.
Step 5: The small finishing touches
Replace public/favicon.svg with this simple coffee cup so the browser tab no longer shows Vite's logo. Then update index.html with a description (what search engines and link previews show) and a theme colour for the phone's browser bar.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#6f4e37"/>
<path d="M16 24h28v14a12 12 0 0 1-12 12h-4a12 12 0 0 1-12-12z" fill="#faf6f1"/>
<path d="M44 28h3a5 5 0 0 1 0 10h-3" fill="none" stroke="#faf6f1" stroke-width="4"/>
<path d="M24 12c-3 3 3 5 0 8M32 12c-3 3 3 5 0 8" fill="none" stroke="#faf6f1" stroke-width="3" stroke-linecap="round"/>
</svg>An SVG icon is tiny and sharp at any size. The file ends in .svg; the block is marked html only for display.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Brew & Bean is a small coffee shop in Indiranagar, Bengaluru. Browse the menu and order ahead for pickup."
/>
<meta name="theme-color" content="#6f4e37" />
<title>Brew & Bean | Coffee shop in Indiranagar, Bengaluru</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>The description is the text search engines often show under your link. Write it for a person.
Test it all
Done when
- At 360 px wide there is no sideways scroll on Home, Menu or Cart.
- The menu shows one column on a phone and several on a laptop.
- Cart rows show the name and price on one line and the quantity and Remove under it.
- Press Tab from the top of Home: the first stop is "Skip to content"; pressing Enter jumps to the main area.
- You can add an item, open the cart, fill in the form and place the order using only the keyboard.
- A clear outline shows which element has focus at every step.
- Submitting the empty form moves the focus to the Name field.
- The browser tab shows a different title on each page.
- Zoom the browser to 200 percent: nothing overlaps or disappears.
- Lighthouse accessibility shows no serious issues you cannot explain.
Hear your site
Try a real screen reader once. On Windows, NVDA is free; on Android, TalkBack is built in (Settings, Accessibility). Even five minutes of listening to your own site teaches you more than reading about it.
git add .
git commit -m "Make the site responsive, accessible and faster"Review this React component for accessibility problems using WCAG 2.2 AA as the standard. List only real issues, ordered by how much they hurt a keyboard or screen reader user. For each, say what a user experiences and give the smallest code fix.
<code>
[paste one component, for example Cart.jsx]
</code>
If you are not sure about an issue, say how to test it in the browser instead of guessing.What to expect: Test every suggestion yourself with the keyboard. AI tools sometimes suggest unnecessary ARIA attributes. Native HTML elements (button, label, nav) are almost always better than extra ARIA.
Try it: Add a dark mode
Use the CSS variables at the top of index.css and the prefers-color-scheme: dark media query to give the site a dark theme when the visitor's device is set to dark. Keep every text colour at 4.5 to 1 contrast or better.
Try it: Stretch: respect reduced motion
Add a short hover transition to .btn (for example background-color 0.2s). Then add a media query for prefers-reduced-motion: reduce that removes it. Explain in one sentence who needs this.
Key takeaways
- Test at 360 px in DevTools; AI-written CSS often only works on a desktop screen.
- Prefer flexible grids (
auto-fill,minmax) and use media queries for the rest. - Accessibility basics: real buttons and labels, skip link, visible focus, good contrast, focus moved after actions, a different title per page.
- Measure speed on the built site with
npm run build,npm run previewand Lighthouse; don't guess. - Code splitting helps larger pages; for most small sites, images are the biggest win.
- Use native HTML first, ARIA second, and test every AI suggestion yourself.
Check yourself
Finishing the quiz marks the lesson as done.
1. What does grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)) do?
2. Why test the speed of npm run preview rather than npm run dev?
3. A keyboard user submits the form with errors but focus stays on the button. What helps most?
4. What is the purpose of the skip link?
5. Lighthouse shows the lazy Cart page saved only a few kilobytes. What is the right conclusion?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.