Skip to main content

Project 02 · Beginner

Color Flipper

Pick a colour format, press the button, and the whole page turns a random colour. Behind it, JavaScript makes random numbers, builds them into a colour string, and hands that string to CSS.

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

Why build it

With random numbers, template literals, and CSS variables, you can give each new user a random avatar colour, build a palette generator for designers, write a theme switcher that recolours a whole site from one value, or add a "Surprise me" button that picks a random recipe or article.

Skills you'll earn

  • Pick a random whole number in any range, for colours, dice, or shuffles
  • Build text from changing values with template literals
  • Turn numbers into hex colour codes with toString(16)
  • Read which radio button someone picked and act on it
  • Restyle a page from JavaScript by changing one CSS variable

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>Color Flipper</title>
		<link rel="stylesheet" href="style.css" />
	</head>
	<body>
		<main class="flipper">
			<h1 class="flipper-title">Color Flipper</h1>

			<!-- A fieldset groups the radios, and its legend names the group. -->
			<fieldset class="format-picker">
				<legend class="format-legend">Colour format</legend>
				<!-- Wrapping an input in its label makes the text clickable too. -->
				<label class="format-option">
					<input type="radio" name="format" value="hex" checked />
					HEX
				</label>
				<label class="format-option">
					<input type="radio" name="format" value="rgb" />
					RGB
				</label>
				<label class="format-option">
					<input type="radio" name="format" value="hsl" />
					HSL
				</label>
			</fieldset>

			<!-- aria-live reads out the new colour after each flip. -->
			<p class="color-value" id="color-value" aria-live="polite">#f4f4f6</p>

			<button type="button" class="flip-button" id="flip">Flip colour</button>
		</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

    Group radio buttons with <fieldset> and <legend>

    Radio buttons let someone pick one option from a set. Radios that share a name, here format, act as one group: picking one unpicks the others. A <fieldset> wraps related inputs, and its <legend> gives the group a name, so screen readers say "Colour format" before reading the options. Without it, someone would hear "HEX" with no idea what it's choosing.

    			<!-- A fieldset groups the radios, and its legend names the group. -->
    			<fieldset class="format-picker">
    				<legend class="format-legend">Colour format</legend>
    				<!-- Wrapping an input in its label makes the text clickable too. -->
    				<label class="format-option">
    					<input type="radio" name="format" value="hex" checked />
    					HEX
    				</label>
    				<label class="format-option">
    					<input type="radio" name="format" value="rgb" />
    					RGB
    				</label>
    				<label class="format-option">
    					<input type="radio" name="format" value="hsl" />
    					HSL
    				</label>
    			</fieldset>
  2. 02

    Wrap each input in its <label>

    A <label> is the text that names a form input. Putting the <input> inside its <label> connects the two, with no id needed. Clicking the word "RGB" then picks the RGB radio, which gives people a much bigger target than the small circle, and screen readers read "RGB" as the radio's name.

    				<!-- Wrapping an input in its label makes the text clickable too. -->
    				<label class="format-option">
    					<input type="radio" name="format" value="hex" checked />
    					HEX
    				</label>
    				<label class="format-option">
    					<input type="radio" name="format" value="rgb" />
    					RGB
    				</label>
    				<label class="format-option">
    					<input type="radio" name="format" value="hsl" />
    					HSL
    				</label>

CSSthe look

  1. 03

    Let JavaScript change one CSS variable

    A CSS custom property is a variable in CSS: you name it with two dashes, like --color, give it a value, and read it anywhere with var(). Here :root sets --color to a light grey, and body uses var(--color) for its background. JavaScript only changes the variable, and CSS still decides where the colour goes.

    /* The one value JavaScript changes. Everything that uses it updates. */
    :root {
    	--color: #f4f4f6;
    }
    
    body {
    	display: grid;
    	place-items: center;
    	min-height: 100vh;
    	margin: 0;
    	background-color: var(--color);
  2. 04

    Respect prefers-reduced-motion

    transition makes the background fade from one colour to the next over 0.4 seconds, instead of jumping. Some people feel dizzy or sick from movement on screen, so they turn on a "reduce motion" setting on their device. The prefers-reduced-motion: reduce media query matches when that setting is on, and sets transition: none so the colour changes at once.

    	transition: background-color 0.4s ease;
    }
    
    /* Skip the fade for people who've asked their device for less motion. */
    @media (prefers-reduced-motion: reduce) {
    	body {
    		transition: none;
    	}
    }
  3. 05

    Style a label from its radio with :has()

    The radio circles are hidden with opacity: 0 so the labels can look like pill buttons. They're see-through, not removed, so keyboards and screen readers still use them. :has() selects an element based on what's inside it: .format-option:has(input:checked) fills in the label whose radio is picked, and :has(input:focus-visible) outlines the label whose radio has keyboard focus.

    /* The real radio stays in the page for keyboards and screen readers. */
    .format-option input {
    	position: absolute;
    	opacity: 0;
    }
    
    /* :has() styles the label based on the radio inside it. */
    .format-option:has(input:checked) {
    	background: #1c1c22;
    	color: #ffffff;
    }
    
    .format-option:has(input:focus-visible) {
    	outline: 3px solid #2563eb;
    	outline-offset: 3px;
    }

JavaScriptthe behaviour

  1. 06

    Make a random whole number

    Math.random() gives a random decimal from 0 up to, but never reaching, 1, like 0.42. Multiplying it by max stretches that range, and Math.floor() rounds down to a whole number. The result is a whole number from 0 to max - 1, so randomNumber(256) gives 0 to 255, the range each colour channel uses. Every colour here is built from these.

    // A whole number from 0 up to, but not including, max.
    function randomNumber(max) {
    	return Math.floor(Math.random() * max);
    }
  2. 07

    Turn numbers into hex with toString(16)

    A hex colour like #ff8800 is three numbers, for red, green and blue, each from 0 to 255. Each is written as two characters in base 16, where digits run 0 to 9 and then a to f. toString(16) converts a number to base 16, so 255 becomes "ff". padStart(2, "0") adds a leading zero when a number needs only one character, so 10 becomes "0a", not "a".

    // Each function builds a colour as a string, in its own format.
    function randomHex() {
    	const channels = [randomNumber(256), randomNumber(256), randomNumber(256)];
    	// toString(16) turns 255 into "ff"; padStart turns "a" into "0a".
    	const pairs = channels.map((channel) => channel.toString(16).padStart(2, "0"));
    	return "#" + pairs.join("");
    }
  3. 08

    Build strings with template literals

    A template literal is a string written with backticks instead of quotes. Inside it, ${} drops in the result of any expression, so randomRgb() places three random numbers straight into rgb(..., ..., ...). Building the same string with + means closing and reopening quotes around every number, which is harder to read and to get right.

    function randomRgb() {
    	return `rgb(${randomNumber(256)}, ${randomNumber(256)}, ${randomNumber(256)})`;
    }
    
    function randomHsl() {
    	return `hsl(${randomNumber(360)}, ${randomNumber(101)}%, ${randomNumber(101)}%)`;
    }
  4. 09

    Read the picked radio with :checked

    :checked is a CSS pseudo-class that matches a radio or checkbox that's picked right now. Because querySelector takes any CSS selector, input[name="format"]:checked finds the picked format radio, and .value reads its value attribute, such as "rgb". It checks the page each time you call it, so you don't need a variable to keep track of the choice.

    // :checked finds whichever radio is selected right now.
    function selectedFormat() {
    	return document.querySelector('input[name="format"]:checked').value;
    }
  5. 10

    Choose a path with if, then update the page

    An if statement runs code only when its condition is true. else if checks another condition, and else catches everything left. flipColor() uses them to call the colour function that matches the picked format. Then setProperty() writes the new colour into --color on the <body>, which repaints the background, and colorValue.textContent shows the same string on the page.

    // One function does the flip, so the button and the radios can share it.
    function flipColor() {
    	const format = selectedFormat();
    
    	let color;
    	if (format === "hex") {
    		color = randomHex();
    	} else if (format === "rgb") {
    		color = randomRgb();
    	} else {
    		color = randomHsl();
    	}
    
    	// CSS reads --color, so this one line repaints the background.
    	document.body.style.setProperty("--color", color);
    	colorValue.textContent = color;
    }
  6. 11

    Reuse one function with the change event

    Elements fire events when things happen to them: a button fires click, and a radio fires change when it becomes the picked one. The button and every radio all run flipColor(), so one function does the work. forEach adds the listener to each radio in formatRadios, which means switching format flips to a colour in that format straight away.

    flipButton.addEventListener("click", flipColor);
    
    // "change" fires when another radio is picked, so the new format shows at once.
    formatRadios.forEach((radio) => {
    	radio.addEventListener("change", flipColor);
    });

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

    Show the colour code in white or black, whichever is easier to read on the new colour.

  2. Challenge 02

    Add a Copy button that copies the colour code.

  3. Challenge 03

    Keep the last five colours, and let people click one to go back to it.

Articles that go further into ideas from this project.

Finished Color Flipper?

Mark it done to track your progress.

You finished Color Flipper!

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.

Read the article (opens in a new tab)