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.
<!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
- 01
Group radio buttons with
<fieldset>and<legend>Radio buttons let someone pick one option from a set. Radios that share a
name, hereformat, 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> - 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 noidneeded. 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
- 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 withvar(). Here:rootsets--colorto a light grey, andbodyusesvar(--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); - 04
Respect
prefers-reduced-motiontransitionmakes 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. Theprefers-reduced-motion: reducemedia query matches when that setting is on, and setstransition: noneso 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; } } - 05
Style a label from its radio with
:has()The radio circles are hidden with
opacity: 0so 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
- 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 bymaxstretches that range, andMath.floor()rounds down to a whole number. The result is a whole number from 0 tomax - 1, sorandomNumber(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); } - 07
Turn numbers into hex with
toString(16)A hex colour like
#ff8800is 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(""); } - 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, sorandomRgb()places three random numbers straight intorgb(..., ..., ...). 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)}%)`; } - 09
Read the picked radio with
:checked:checkedis a CSS pseudo-class that matches a radio or checkbox that's picked right now. BecausequerySelectortakes any CSS selector,input[name="format"]:checkedfinds the picked format radio, and.valuereads itsvalueattribute, 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; } - 10
Choose a path with
if, then update the pageAn
ifstatement runs code only when its condition is true.else ifchecks another condition, andelsecatches everything left.flipColor()uses them to call the colour function that matches the picked format. ThensetProperty()writes the new colour into--coloron the<body>, which repaints the background, andcolorValue.textContentshows 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; } - 11
Reuse one function with the
changeeventElements fire events when things happen to them: a button fires
click, and a radio fireschangewhen it becomes the picked one. The button and every radio all runflipColor(), so one function does the work.forEachadds the listener to each radio informatRadios, 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.
- Challenge 01
Show the colour code in white or black, whichever is easier to read on the new colour.
- Challenge 02
Add a Copy button that copies the colour code.
- Challenge 03
Keep the last five colours, and let people click one to go back to it.
Learn more on the blog
Articles that go further into ideas from this project.
- Using CSS Variables like a ProHow CSS variables work, from scope and fallbacks to inheritance, and how JavaScript can change them.Read the article (opens in a new tab)
- How to Use Inline CSS Variables in HTMLSetting a CSS variable on one element with its style attribute, and why that's useful.Read the article (opens in a new tab)
- all vs root vs html vs body selectorsWhat :root selects, how it differs from html, body, and *, and why page-wide variables go on it.Read the article (opens in a new tab)
Finished Color Flipper?
Mark it done to track your progress.
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.