Responsive component
This card changes from a stacked layout to a side-by-side layout when its container becomes wide enough.
View patternWorking examples I can come back to when I need to see the pieces together.
Pattern 01
A basic page structure using header, navigation, main content, sections, and footer.
<body>
<header>
<h1>Page Title</h1>
</header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>
<section>
<h2>Featured Content</h2>
<p>Page content...</p>
</section>
</main>
<footer>
<p>Copyright 2026</p>
</footer>
</body>
Pattern 02
A table with a caption, column headers, and row headers.
<table>
<caption>
Monthly project hours
</caption>
<thead>
<tr>
<th scope="col">Project</th>
<th scope="col">Hours</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Portfolio</th>
<td>12</td>
<td>Complete</td>
</tr>
<tr>
<th scope="row">Reference Site</th>
<td>18</td>
<td>In Progress</td>
</tr>
</tbody>
</table>
Pattern 03
A simple form with labels, useful input types, and required fields.
<form>
<div>
<label for="name">
Name
</label>
<input
type="text"
id="name"
name="name"
autocomplete="name"
required
/>
</div>
<div>
<label for="email">
Email
</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
/>
</div>
<div>
<label for="message">
Message
</label>
<textarea
id="message"
name="message"
rows="5"
></textarea>
</div>
<button type="submit">
Send Message
</button>
</form>
Pattern 04
A Flexbox navigation that can wrap when the page gets narrow.
.navigation {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 2rem;
}
.navigation ul {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
Pattern 05
A Grid layout that changes the number of columns based on the available space.
.card-grid {
display: grid;
grid-template-columns:
repeat(
auto-fit,
minmax(250px, 1fr)
);
gap: 1.5rem;
}
Pattern 06
A reusable card using the same spacing, borders, and colors as this site.
<article class="card">
<h3>CSS Grid</h3>
<p>
Learn how grid tracks, gaps,
and placement work together.
</p>
<a href="#">
View Reference
</a>
</article>
.card {
padding: 1.5rem;
background: #ffffff;
border: 1px solid #b9dce5;
border-top: 5px solid #3c88a3;
border-radius: 0.75rem;
}
.card h3 {
color: #3c88a3;
}
.card a {
color: #4eabcc;
font-weight: 600;
}
Pattern 08
A keyboard shortcut that jumps past repeated navigation to the main content.
<a class="skip-link" href="#main-content">
Skip to main content
</a>
<main id="main-content">
...
</main>
Pattern 09
A clear focus style so keyboard users can see which control is active.
:focus-visible {
outline: 3px solid #276a80;
outline-offset: 3px;
}
@media (prefers-color-scheme: dark) {
:focus-visible {
outline-color: #baf553;
}
}
Pattern 10
A media query that reduces animation when the user has reduced motion turned on.
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Pattern 11
A modal built with the native dialog element and a clear way to close it.
Live result
<button type="button" id="open-dialog">
Open dialog
</button>
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Save changes?</h2>
<p>Your changes are ready to save.</p>
<button type="button" id="close-dialog">Cancel</button>
</dialog>
const dialog = document.querySelector("#confirm-dialog");
document.querySelector("#open-dialog").addEventListener("click", () => {
dialog.showModal();
});
document.querySelector("#close-dialog").addEventListener("click", () => {
dialog.close();
});
Pattern 12
An example of loading, success, empty, and error states for an API request.
Live result
let controller;
async function loadProjects() {
controller?.abort();
controller =
new AbortController();
setStatus("Loading…");
try {
const response = await fetch(
"/api/projects",
{
signal: controller.signal
}
);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}`
);
}
const projects =
await response.json();
if (!projects.length) {
setStatus(
"No projects found."
);
return;
}
renderProjects(projects);
} catch (error) {
if (error.name === "AbortError") {
setStatus(
"Request cancelled."
);
return;
}
setStatus(
"Projects could not be loaded."
);
}
}
function cancelRequest() {
controller?.abort();
}
Pattern 13
A small list that filters as the user types and shows how many items match.
Live result
5 items
const input = document.querySelector("#skill-filter");
const items = [...document.querySelectorAll(".skill-list li")];
const status = document.querySelector("#filter-status");
input.addEventListener("input", () => {
const query = input.value.trim().toLowerCase();
let visible = 0;
items.forEach((item) => {
const matches = item.textContent.toLowerCase().includes(query);
item.hidden = !matches;
if (matches) visible += 1;
});
status.textContent = `${visible} item${visible === 1 ? "" : "s"}`;
});
Pattern 14
An accordion using native details and summary elements without extra JavaScript.
Live result
It means choosing elements based on the meaning and role of the content.
No. Native details and summary already provide disclosure behavior.
<details>
<summary>What does semantic HTML mean?</summary>
<p>
Choose elements based on the meaning and role
of the content.
</p>
</details>
Pattern 15
A button that switches a small example between light and dark themes.
Live result
This preview switches independently from the site's current theme.
const toggle = document.querySelector("#theme-toggle");
const root = document.documentElement;
toggle.addEventListener("click", () => {
const nextTheme =
root.dataset.theme === "dark" ? "light" : "dark";
root.dataset.theme = nextTheme;
localStorage.setItem("theme", nextTheme);
});
:root {
--background: #ffffff;
--text: #1c2529;
}
html[data-theme="dark"] {
--background: #11191d;
--text: #f4f8f9;
}
body {
background: var(--background);
color: var(--text);
}
Pattern 16
A short status message shown after an action.
Live result
<button type="button" id="save-button">
Save changes
</button>
<div id="save-status" role="status" aria-live="polite"></div>
saveButton.addEventListener("click", async () => {
await saveChanges();
saveStatus.textContent = "Changes saved.";
});
Pattern 17
A loading placeholder that keeps the same basic space as the finished content.
Live result
.skeleton {
border-radius: 0.35rem;
background: linear-gradient(
90deg,
var(--surface-alt),
var(--highlight),
var(--surface-alt)
);
background-size: 200% 100%;
animation: skeleton-shift 1.4s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
}
}
Pattern 18
A simple empty state that explains why nothing is there and gives the user a next step.
Live result
Save useful entries and they will appear here for quick access.
Browse CSS properties<section class="empty-state" aria-labelledby="empty-title">
<h2 id="empty-title">No saved references yet</h2>
<p>
Save useful entries and they will appear here
for quick access.
</p>
<a href="/references">Browse references</a>
</section>
Pattern 19
Form messages shown close to the field, plus an overall submission message.
Live result
form.addEventListener("submit", (event) => {
event.preventDefault();
if (!email.validity.valid) {
emailError.textContent = "Enter a valid email address.";
email.focus();
return;
}
emailError.textContent = "";
formStatus.textContent = "Form submitted successfully.";
});
Pattern 20
A breadcrumb that shows where the current page sits in the site.
Live result
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/css">CSS</a></li>
<li aria-current="page">Grid</li>
</ol>
</nav>
Pattern 21
A user preference saved in local storage so it can stay available after the page reloads.
Live result
Compact view: Off
const savedSetting =
localStorage.getItem(
'pattern-compact-view'
);
if (savedSetting === 'true') {
setting.classList.add(
'is-compact'
);
}
toggle.addEventListener(
'click',
() => {
const compact =
setting.classList.toggle(
'is-compact'
);
localStorage.setItem(
'pattern-compact-view',
String(compact)
);
}
);
Pattern 22
One event listener on a parent element handles actions from its child elements, including items added later.
Live result
3 items
const list =
document.querySelector(
'[data-delegation-list]'
);
list.addEventListener(
'click',
event => {
const button =
event.target.closest(
'[data-remove-item]'
);
if (!button) {
return;
}
button.closest('li')?.remove();
}
);
Pattern 23
A reusable card that changes its layout based on the space available inside its container.
Live result
This card changes from a stacked layout to a side-by-side layout when its container becomes wide enough.
View pattern<div class="card-container">
<article class="responsive-card">
<div class="card-icon">
CSS
</div>
<div class="card-content">
<h3>Responsive component</h3>
<p>
Component content...
</p>
</div>
</article>
</div>
.card-container {
container-type: inline-size;
}
.responsive-card {
display: grid;
gap: 1rem;
}
@container (min-width: 30rem) {
.responsive-card {
grid-template-columns:
auto 1fr;
align-items: center;
}
}
Pattern 24
A responsive image that can provide different sources, sizes, and cropping behavior for different layouts.
Live result
<picture>
<source
media="(min-width: 60rem)"
srcset="wide-image.webp"
/>
<img
src="small-image.webp"
srcset="
small-image.webp 480w,
large-image.webp 1200w
"
sizes="
(min-width: 60rem) 50vw,
100vw
"
alt="Descriptive alternative text"
loading="lazy"
width="1200"
height="675"
/>
</picture>
.responsive-media img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
Pattern 25
A related set of choices grouped with a fieldset and legend so the options have shared context.
Live result
<fieldset
aria-describedby="contact-help"
>
<legend>
Preferred contact method
</legend>
<p id="contact-help">
Choose one option.
</p>
<label>
<input
type="radio"
name="contact-method"
value="email"
/>
Email
</label>
<label>
<input
type="radio"
name="contact-method"
value="phone"
/>
Phone
</label>
</fieldset>