Build a React color picker whose selected color drives a preview, a hex label, and the highlighted swatch. The fixed six-color palette already renders in the starter. Your job is to add one state value and connect the buttons to it.
Implement the default-exported App component in App.tsx. It receives no props. Track the chosen hex string with useState, starting at COLORS[0], and update it from each swatch button's onClick.
#ef4444.#22c55e, and moves the ring onto the green swatch.aria-pressed from selected.aria-label and update aria-pressed from the same state.styles.css.You'll hold the chosen color in useState and let it drive the preview, the hex label, and the ring together.
Six swatches, one preview. Clicking a swatch should recolor the preview, update the hex text, and highlight the swatch you picked. The trap is treating those as three separate things to update on every click. They're one thing — selected — rendered three ways.
Keep exactly one piece of state: the currently selected color. Everything visible is a function of it. The preview background is selected, the label text is selected, and a swatch is ringed when its own color equals selected. Change the one value and React re-renders all three; there's no per-swatch "am I selected" flag to keep in sync.
A common first try reaches into the DOM to recolor things by hand:
function handleClick(e) {
const preview = document.querySelector('.preview');
preview.style.background = e.currentTarget.style.background; // imperative poke
}
The preview even recolors. But there's no state, so the hex label never updates, the ring can't move (which swatch is selected?), and nothing else in the component can ask "what's chosen?". In React the DOM should be a consequence of state, not the place you store it.
import { useState } from 'react';
import './styles.css';
const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'];
export default function App() {
const [selected, setSelected] = useState(COLORS[0]);
return (
<main className="container">
<h1>Color Swatch Picker</h1>
<div className="preview" style={{ background: selected }} aria-hidden="true" />
<p className="hex" aria-live="polite">{selected}</p>
<div className="swatches" role="group" aria-label="Choose a color">
{COLORS.map((c) => (
<button
key={c}
type="button"
className={c === selected ? 'swatch selected' : 'swatch'}
style={{ background: c }}
aria-label={`Select ${c}`}
aria-pressed={c === selected}
onClick={() => setSelected(c)}
/>
))}
</div>
</main>
);
}
selected is the single source of truth. Each swatch's onClick sets it to that swatch's color. The preview, label, ring, and aria-pressed all derive from the value during the same render. The palette remains a constant because clicking never changes the available colors.
selected = '#ef4444' → preview red, label #ef4444, ring on the red swatch.setSelected('#22c55e') → re-render → preview green, label #22c55e, and now c === selected is true only for the green swatch, so the ring moves there.selected flag per swatch — storing "am I picked?" on each swatch means clearing the old one on every click. Derive the ring from one selected value instead.preview.style.background = ... skips the label and the ring. Drive them all from state.c === selected (color to color); comparing indices works too but breaks if the palette is reordered.aria-pressed={c === selected} to expose the same state to assistive technology.value + onChange props so a parent owns the selection (the standard controlled-input pattern).<input type="color"> that feeds into the same selected state.This version uses a reducer for the selected value. It keeps the palette constant and renders the exact same preview, label, button classes, and pressed states.
import { useReducer } from 'react';
import './styles.css';
const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'];
function chooseColor(_current: string, next: string): string {
return next;
}
export default function App() {
const [selected, select] = useReducer(chooseColor, COLORS[0]);
return (
<main className="container">
<h1>Color Swatch Picker</h1>
<div className="preview" style={{ background: selected }} aria-hidden="true" />
<p className="hex" aria-live="polite">{selected}</p>
<div className="swatches" role="group" aria-label="Choose a color">
{COLORS.map((color) => {
const active = color === selected;
return (
<button
key={color}
type="button"
className={active ? 'swatch selected' : 'swatch'}
style={{ background: color }}
aria-label={`Select ${color}`}
aria-pressed={active}
onClick={() => select(color)}
/>
);
})}
</div>
</main>
);
}The reducer replaces one string, while a local active value projects that state consistently onto both visual and accessible button attributes.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a React color picker whose selected color drives a preview, a hex label, and the highlighted swatch. The fixed six-color palette already renders in the starter. Your job is to add one state value and connect the buttons to it.
Implement the default-exported App component in App.tsx. It receives no props. Track the chosen hex string with useState, starting at COLORS[0], and update it from each swatch button's onClick.
#ef4444.#22c55e, and moves the ring onto the green swatch.aria-pressed from selected.aria-label and update aria-pressed from the same state.styles.css.You'll hold the chosen color in useState and let it drive the preview, the hex label, and the ring together.
Six swatches, one preview. Clicking a swatch should recolor the preview, update the hex text, and highlight the swatch you picked. The trap is treating those as three separate things to update on every click. They're one thing — selected — rendered three ways.
Keep exactly one piece of state: the currently selected color. Everything visible is a function of it. The preview background is selected, the label text is selected, and a swatch is ringed when its own color equals selected. Change the one value and React re-renders all three; there's no per-swatch "am I selected" flag to keep in sync.
A common first try reaches into the DOM to recolor things by hand:
function handleClick(e) {
const preview = document.querySelector('.preview');
preview.style.background = e.currentTarget.style.background; // imperative poke
}
The preview even recolors. But there's no state, so the hex label never updates, the ring can't move (which swatch is selected?), and nothing else in the component can ask "what's chosen?". In React the DOM should be a consequence of state, not the place you store it.
import { useState } from 'react';
import './styles.css';
const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'];
export default function App() {
const [selected, setSelected] = useState(COLORS[0]);
return (
<main className="container">
<h1>Color Swatch Picker</h1>
<div className="preview" style={{ background: selected }} aria-hidden="true" />
<p className="hex" aria-live="polite">{selected}</p>
<div className="swatches" role="group" aria-label="Choose a color">
{COLORS.map((c) => (
<button
key={c}
type="button"
className={c === selected ? 'swatch selected' : 'swatch'}
style={{ background: c }}
aria-label={`Select ${c}`}
aria-pressed={c === selected}
onClick={() => setSelected(c)}
/>
))}
</div>
</main>
);
}
selected is the single source of truth. Each swatch's onClick sets it to that swatch's color. The preview, label, ring, and aria-pressed all derive from the value during the same render. The palette remains a constant because clicking never changes the available colors.
selected = '#ef4444' → preview red, label #ef4444, ring on the red swatch.setSelected('#22c55e') → re-render → preview green, label #22c55e, and now c === selected is true only for the green swatch, so the ring moves there.selected flag per swatch — storing "am I picked?" on each swatch means clearing the old one on every click. Derive the ring from one selected value instead.preview.style.background = ... skips the label and the ring. Drive them all from state.c === selected (color to color); comparing indices works too but breaks if the palette is reordered.aria-pressed={c === selected} to expose the same state to assistive technology.value + onChange props so a parent owns the selection (the standard controlled-input pattern).<input type="color"> that feeds into the same selected state.This version uses a reducer for the selected value. It keeps the palette constant and renders the exact same preview, label, button classes, and pressed states.
import { useReducer } from 'react';
import './styles.css';
const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#8b5cf6', '#ec4899'];
function chooseColor(_current: string, next: string): string {
return next;
}
export default function App() {
const [selected, select] = useReducer(chooseColor, COLORS[0]);
return (
<main className="container">
<h1>Color Swatch Picker</h1>
<div className="preview" style={{ background: selected }} aria-hidden="true" />
<p className="hex" aria-live="polite">{selected}</p>
<div className="swatches" role="group" aria-label="Choose a color">
{COLORS.map((color) => {
const active = color === selected;
return (
<button
key={color}
type="button"
className={active ? 'swatch selected' : 'swatch'}
style={{ background: color }}
aria-label={`Select ${color}`}
aria-pressed={active}
onClick={() => select(color)}
/>
);
})}
</div>
</main>
);
}The reducer replaces one string, while a local active value projects that state consistently onto both visual and accessible button attributes.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.