Color Swatch
Prompt
Build a color palette in React that displays a grid of color swatches. Clicking a swatch selects it and shows a larger preview with its name and hex value. A "Copy" button copies the hex value to the clipboard and shows brief "Copied!" feedback.
Requirements
- Display a grid of color swatches from the provided data
- Clicking a swatch selects it and shows a larger preview area with the color
- Display the selected color's name and hex value below the preview
- A "Copy" button copies the hex value to the clipboard
- Show "Copied!" feedback on the button for a short time after copying
Example

Playground
How would you arrange the swatches in a grid? Think about which CSS layout method handles rows and columns well. Also, what HTML element makes each swatch keyboard-accessible by default?
What piece of data uniquely identifies each color? If the colors array were to change in the future (filtered, reordered, or extended), would your selection still point to the right color? How would you look up the full color object from that identifier?
Look into navigator.clipboard.writeText(). It is asynchronous. How would you show temporary feedback on the button after the copy succeeds, and automatically hide it after a short delay?
Solution
Explanation
Project Structure
We split the code into focused files:
App.jsholds the color data and rendersColorSwatchcomponents/ColorSwatch.jsmanages selection state and composes the grid and previewcomponents/Swatch.jsis a presentational component for each color buttoncomponents/ColorPreview.jsshows the selected color's preview, name, hex value, and copy button
Swatch Grid Layout
The grid uses CSS Grid with repeat(5, 1fr) to create a 5-column layout. Each swatch uses aspect-ratio: 1 to maintain a square shape:
.grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.swatch {
aspect-ratio: 1;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
}Alternative: Flexbox layout
If you are not familiar with CSS Grid, you can achieve the same layout using Flexbox with flex-wrap: wrap and a fixed width on each swatch:
.grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.swatch {
width: 60px;
height: 60px;
}The tradeoff is that Grid automatically sizes columns to fill the container (1fr), while Flexbox requires a fixed swatch size. Both are perfectly valid in an interview.
The transparent border is important. When a swatch is selected, we change the border color to black. If we only added a border on selection, the swatch would shift by 2px (layout shift). Starting with a transparent border of the same width avoids this.
Selection State
Selection is tracked by the hex value, not an array index:
const [selectedHex, setSelectedHex] = useState(null);
const selectedColor = colors.find(
(c) => c.hex === selectedHex
);We derive selectedColor using find rather than storing the entire color object. This keeps the state minimal. The ColorPreview component renders conditionally based on whether a color is selected:
Why select by hex instead of index?
Selecting by index is fragile. If the colors array is reordered, filtered, or items are added/removed, the index would point to the wrong color. Selecting by a unique identifier (the hex value) keeps the selection stable regardless of how the array changes. This is how you build reusable, scalable components. Think about the future: what if this palette supports user-added colors or a search filter? Index-based selection would break. Value-based selection keeps working.
{
selectedColor && <ColorPreview color={selectedColor} />;
}This keeps ColorSwatch focused on selection logic. All copy and feedback logic lives inside ColorPreview.
Copy to Clipboard
The navigator.clipboard.writeText() API copies text to the clipboard. It returns a Promise that resolves when the copy succeeds:
const handleCopy = () => {
navigator.clipboard.writeText(color.hex).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 1500);
});
};The ColorPreview component owns both the isCopied state and the handleCopy function. Because ColorPreview only renders when a color is selected, there is no need for a null guard on the color prop.
The "Copied!" Feedback
After copying, we show "Copied!" on the button for 1.5 seconds using a simple setTimeout. This is the most straightforward approach: set isCopied to true, then schedule it back to false.
If the user clicks "Copy" rapidly, multiple timeouts will overlap. The worst that happens is "Copied!" disappears based on the first click's timer rather than the latest. For a simple color palette this is perfectly acceptable.
The Swatch Component
const Swatch = memo(function Swatch({
color,
isSelected,
onClick,
}) {
return (
<button
className={`swatch ${isSelected ? 'selected' : ''}`}
style={{ backgroundColor: color.hex }}
onClick={onClick}
aria-label={color.name}
title={color.name}
/>
);
});Each swatch is a button element, not a div. This is important for accessibility because buttons are focusable and activatable by keyboard by default. The aria-label provides the color name for screen readers since the button has no visible text. The title shows a tooltip on hover.
React.memo wraps the component because only the selected swatch's isSelected prop changes on each click. The other 9 swatches receive the same props and skip re-rendering.
Production note: Clipboard API in iframes
In production, navigator.clipboard.writeText() can be blocked by the browser's permissions policy in cross-origin iframes. If your app runs inside an iframe, you may need a fallback using a temporary textarea with document.execCommand('copy'). For an interview, navigator.clipboard.writeText() alone is perfectly sufficient.
Interviewer Criteria
HTML & CSS
Swatches are laid out with CSS Grid or Flexbox and maintain a square aspect ratio.
Selected swatch has a visible border that does not cause layout shift (transparent border used as placeholder).
Preview section displays the color as a large rectangle with the name and hex value below it.
CSS variables used for shared colors like border, secondary text, and success.
JavaScript
Uses navigator.clipboard.writeText() to copy the hex value to the clipboard.
"Copied!" feedback uses setTimeout to reset the button text after a short delay.
React
Selection tracked by hex value instead of array index, making the component resilient to array changes.
Split into focused components: Swatch for each color button, ColorPreview for the preview and copy logic, and ColorSwatch for selection state.
Wrapped Swatch in React.memo to skip re-renders for unselected swatches.
Accessibility
Each swatch is a button element, making it keyboard focusable and activatable.
aria-label provides the color name for screen readers since the button has no visible text.
title attribute shows the color name on hover as a tooltip.
Code Quality
Color data is passed as a prop rather than imported directly, making the component reusable.
State is minimal: only the selected hex is stored, the color object is derived with find.
Clean separation: data in App.js, selection in ColorSwatch.js, preview and copy in ColorPreview.js, presentation in Swatch.js.
Variable, function, and component names clearly communicate intent: selectedHex, isCopied, handleCopy, ColorPreview, Swatch.
Time Checkpoints
- 10:00 AM
Interview starts 👥
- 10:03 AM
Prompt given by the interviewer
- 10:05 AM
Candidate reads the prompt, asks clarifying questions, and starts coding
- 10:12 AM
Render the grid of color swatches with CSS Grid or Flexbox
- 10:18 AM
Selection state: clicking a swatch shows the preview area with color name and hex
- 10:25 AM
Copy button with navigator.clipboard.writeText and Copied! feedback
- 10:32 AM
Extract Swatch, ColorPreview components with clean props
- 10:38 AM
Polish: selected border, hover effects, accessibility attributes
- 10:42 AM
Edge cases: transparent border for layout shift, React.memo on Swatch
- 10:45 AM
Discussions with interviewer
- 10:45 AM
Interview ends ✅