Skip to main content

Project 15 · Advanced

Random Quote

A quote card with a New quote button. It asks a free public API for a fresh quote, fades the old one while it waits, and lets you copy the quote in one click.

  • 2 HTML ideas
  • 2 CSS ideas
  • 6 JavaScript ideas

Why build it

Loading data from an API with fetch lets you build a weather widget, a news feed, a currency converter with live rates, or a page that lists your latest GitHub repositories. You'll also learn to keep the page usable while it waits, and when the network is slow or fails, so what you build still works for people on a bad connection.

Skills you'll earn

  • Request data from an API with fetch
  • Wait for an answer without freezing the page, with async and await
  • Check every response and handle errors with try, catch, and finally
  • Show loading and error states that screen readers announce too
  • Copy text for someone with the Clipboard API

01

Try it

Play with it first, then read the code that makes it work.

Result0px
New tab (opens in a new tab)
Download
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Random Quote</title>
		<link rel="stylesheet" href="style.css" />
	</head>
	<body>
		<main class="card">
			<h1 class="card-title">Quote of the moment</h1>

			<!-- aria-live reads each new quote; aria-busy holds it back while loading. -->
			<figure class="quote" id="quote" aria-live="polite">
				<blockquote class="quote-text" id="quote-text">
					<p>Loading a quote…</p>
				</blockquote>
				<figcaption class="quote-author" id="quote-author"></figcaption>
			</figure>

			<!-- role="alert" reads an error out straight away. -->
			<p class="error" id="error" role="alert" hidden></p>

			<div class="actions">
				<button type="button" class="button button-primary" id="new-quote">
					New quote
				</button>
				<button type="button" class="button" id="copy">Copy</button>
			</div>
			<p class="copied" id="copied" role="status"></p>
		</main>

		<script src="script.js"></script>
	</body>
</html>

02

How it works

The ideas behind it, one at a time, each next to the code that does it.

HTMLthe structure

  1. 01

    Mark up a quote with <blockquote> and <figcaption>

    <blockquote> marks text as a quotation, and <figcaption> gives a caption, here the name of who said it. Wrapping both in a <figure> ties the caption to the quote, so browsers and screen readers know they belong together. The figure also has aria-live="polite", so each new quote is read out when it appears.

    			<!-- aria-live reads each new quote; aria-busy holds it back while loading. -->
    			<figure class="quote" id="quote" aria-live="polite">
    				<blockquote class="quote-text" id="quote-text">
    					<p>Loading a quote…</p>
    				</blockquote>
    				<figcaption class="quote-author" id="quote-author"></figcaption>
    			</figure>
  2. 02

    Announce errors with role="alert"

    role="alert" makes an element an urgent live region: screen readers read its text the moment it appears, even interrupting what they're already reading. That's too pushy for normal updates, so it's kept for problems people need to know about now. Here it's the error message, which starts hidden and only shows when a quote fails to load.

    			<!-- role="alert" reads an error out straight away. -->
    			<p class="error" id="error" role="alert" hidden></p>

CSSthe look

  1. 03

    Add decoration screen readers skip

    The ::before pseudo-element adds content before an element using only CSS, here a big quote mark above the quote. Screen readers can still read CSS content, so they'd say "left double quotation mark" every time. Writing content: "“" / "" gives it empty alternative text after the slash, so it's shown but not read out.

    /* A big decorative quote mark, drawn by CSS so screen readers skip it. */
    .quote-text::before {
    	content: "“" / "";
    	display: block;
    	height: 2.5rem;
    	font-family: Georgia, serif;
    	font-size: 4rem;
    	line-height: 1;
    	color: #d4d4da;
    }
  2. 04

    Show loading with aria-busy

    aria-busy="true" tells screen readers an area is being updated, so they can wait for the new content before reading it. CSS can select elements by attribute, so .quote[aria-busy="true"] uses the same attribute to fade the old quote to 40% opacity. One attribute tells both screen reader users and sighted users that something's happening.

    /* While a quote loads, fade the old one so it's clear something is happening. */
    .quote[aria-busy="true"] {
    	opacity: 0.4;
    	transition: opacity 0.2s ease;
    }

JavaScriptthe behaviour

  1. 05

    Track the loading state

    isLoading is a let variable that remembers whether a quote is on its way. setLoading() updates it, sets aria-busy on the quote, and switches the button text to "Loading…" and back. At the start of loadQuote(), if (isLoading) return; ignores extra clicks. The button isn't disabled, because a disabled button loses keyboard focus.

    let currentQuote = null;
    let isLoading = false;
    
    // Fade the old quote and change the button text while waiting.
    function setLoading(loading) {
    	isLoading = loading;
    	quote.setAttribute("aria-busy", loading);
    	newQuoteButton.textContent = loading ? "Loading…" : "New quote";
    }
    
    function showQuote({ text, author }) {
    	const paragraph = document.createElement("p");
    	paragraph.textContent = text;
    	quoteText.replaceChildren(paragraph);
    	quoteAuthor.textContent = author;
    }
    
    // async lets the function wait for the network with await, and the page
    // keeps working in the meantime.
    async function loadQuote() {
    	// Ignore extra clicks while a quote is already on its way.
    	if (isLoading) return;
    	setLoading(true);
  2. 06

    Wait for the network with async and await

    Asking a server for data takes time, so fetch() doesn't return the answer straight away. It returns a promise: a placeholder for a value that arrives later. Inside a function marked async, await pauses that function until the promise settles, then hands back the value. The rest of the page keeps working while it waits.

    // async lets the function wait for the network with await, and the page
    // keeps working in the meantime.
    async function loadQuote() {
    	// Ignore extra clicks while a quote is already on its way.
    	if (isLoading) return;
    	setLoading(true);
    	errorMessage.hidden = true;
    
    	try {
    		const response = await fetch(API_URL);
  3. 07

    Check response.ok before using the answer

    fetch() only throws an error when the request can't reach the server at all, like when you're offline. If the server answers with an error, such as 404 (not found) or 500 (server error), it still counts as a response. response.ok is true only for success codes, so the code checks it and throws its own error when it's false.

    		// fetch only fails on a network error; a 404 or 500 still arrives, so check.
    		if (!response.ok) {
    			throw new Error(`The server answered ${response.status}`);
    		}
  4. 08

    Read the data with .json()

    JSON is a text format for sending data that looks like JavaScript objects. The response body arrives as that text, and response.json() reads it and turns it into a real object. Reading the body takes time too, so it's awaited. After that, data.quote and data.author can be read like any other object's properties.

    		// The body arrives as text; .json() turns it into an object.
    		const data = await response.json();
    		currentQuote = { text: data.quote, author: data.author };
    		showQuote(currentQuote);
  5. 09

    Handle failure with try, catch, and finally

    try runs code that might fail. If anything inside it throws, such as a network error or the error thrown for a bad response, the code jumps to catch, which shows a friendly message instead of leaving the page stuck. finally runs afterwards either way, so setLoading(false) always ends the loading state.

    	try {
    		const response = await fetch(API_URL);
    		// fetch only fails on a network error; a 404 or 500 still arrives, so check.
    		if (!response.ok) {
    			throw new Error(`The server answered ${response.status}`);
    		}
    		// The body arrives as text; .json() turns it into an object.
    		const data = await response.json();
    		currentQuote = { text: data.quote, author: data.author };
    		showQuote(currentQuote);
    	} catch {
    		errorMessage.textContent =
    			"Couldn't load a quote. Check your connection and try again.";
    		errorMessage.hidden = false;
    	} finally {
    		// finally runs whether it worked or not, so loading always ends.
    		setLoading(false);
    	}
  6. 10

    Copy text with the Clipboard API

    The Clipboard API lets a page copy text for you. navigator.clipboard.writeText() returns a promise, so it's awaited. The browser can refuse, for example when the page isn't served securely, so it has its own try and catch, with a message for each case in a role="status" element. setTimeout() clears that message after 2.5 seconds.

    copyButton.addEventListener("click", async () => {
    	if (!currentQuote) return;
    	try {
    		await navigator.clipboard.writeText(
    			`"${currentQuote.text}" — ${currentQuote.author}`,
    		);
    		copiedMessage.textContent = "Copied to your clipboard.";
    	} catch {
    		copiedMessage.textContent = "Couldn't copy. Select the quote and copy it instead.";
    	}
    	// Clear the message after a moment, ready for the next copy.
    	setTimeout(() => {
    		copiedMessage.textContent = "";
    	}, 2500);
    });

03

Wrap up

Track your progress, then go deeper or keep going.

Take it further

Once it works, try these on your own. Each one uses what you've just learned, plus one new idea.

  1. Challenge 01

    Add a Share button that uses the Web Share API, with Copy as the fallback.

  2. Challenge 02

    Keep the last five quotes so people can go back to one.

  3. Challenge 03

    Let people save favourite quotes in localStorage and list them under the card.

Finished Random Quote?

Mark it done to track your progress.

You finished Random Quote!

0 of 15 projects done.

Go deeper

The full article explains each idea in more depth, why the code is written this way, and more than fits on this page.

Article coming soon. See the series so far (opens in a new tab)