Skip to main content

Refactoring UI

Project preview where I put the principles of Refactoring UI into practice

Refactoring UI in Practice

Go to Project ↗

Copy-paste components,
zero dependencies

The Project

A practical case study on the book Refactoring UI by Adam Wathan and Steve Schoger: each component is built by directly translating a principle from the book into code. No framework, no build step, three simple files: index.html, styles.css, script.js.

Source Code

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Refactoring UI Study | UX Engineer Log</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap">
<link rel="stylesheet" href="styles.css">
</head>

<body>

<!--
DESIGN
------
* This is a single-page component showcase built to explore and apply the concepts
* learned from Refactoring UI (Adam Wathan and Steve Schoger).
* It has no build step, just three plain files: this one, styles.css, and script.js
* The architecture follows a strict separation of concerns:
* - index.html (this file) contains only the structure and content
* - styles.css handles every visual rule, token, and state
* - script.js implements the copy-to-clipboard behavior and checkmark animation
*
* SVG SPRITE PATTERN (<symbol id="icon-copy">):
* - A "sprite" acts as a hidden catalog of reusable graphics.
* Here, I define the copy icon's path only once at the top of the <body>,
* inside a visually hidden <svg> definition
* - Maintainability and performance: All 16 copy buttons reference this single
* definition via <use href="#icon-copy">. This keeps the DOM clean and
* creates a Single Source of Truth. If the icon design changes, I only
* need to update that one hidden symbol
* - Coordinate space stability: The outer <svg> wrapper on each button strictly
* enforces viewBox="0 0 26 26". This guarantees a stable drawing canvas,
* ensuring the icon never warps or shifts, even when JavaScript dynamically
* swaps the SVG content (from the copy icon to the success checkmark)
*
* ACCESSIBILITY:
* - Hidden labels: Every input has a <label for="..."> with class="sr-only".
* While sighted users understand the input's purpose from the visual layout,
* screen readers need real text. The 'for' attribute links the label to the
* input so blind users know exactly what the field is for
* - Semantic grouping: The radio buttons are wrapped in a <fieldset> with a
* hidden <legend>. Without this, a screen reader would just announce the
* options without explaining the main topic. The <legend> acts as a title
* for the group, telling the user exactly what choice they are making
* - Decorative elements: SVGs used only for visual appeal carry aria-hidden="true".
* Without this, screen readers might announce meaningless "image" or "graphic"
* labels. This attribute hides the icon from the accessibility tree, eliminating
* audio clutter and keeping the focus on the actual text or action
* - Contextual actions: Every copy button has a unique aria-label (e.g.
* "Copy primary button color"). If we just used "Copy" for all 16 buttons,
* a blind user tabbing through the page wouldn't know what they are copying
*
* PAGE STRUCTURE:
* 0. STYLESHEET: components.css preview and one-click copy
* 1. BUTTON: Primary, Secondary, Tertiary
* 2. INPUT: Alone, With Checkbox
* 3. RADIO BUTTON
* 4. CARD: 5 shadow levels
* 5. ICON: Original (128px), Simplified (128px), Shrunk (16px in frame)
* 6. EMPTY STATE
-->

<!--
Shared SVG sprite:
Defines the copy icon paths once to keep the DOM clean. All copy buttons
reference it dynamically via <use href="#icon-copy">. While 'display: none'
removes it from the visual layout, 'aria-hidden="true"' acts as a defensive
fallback to ensure screen readers never announce it
-->
<svg aria-hidden="true" style="display:none">
<defs>
<symbol id="icon-copy" viewBox="0 0 26 26" fill="none">
<path d="M22.6 8.20001H10.6C9.2745 8.20001 8.19998 9.27453 8.19998 10.6V22.6C8.19998 23.9255 9.2745 25 10.6 25H22.6C23.9255 25 25 23.9255 25 22.6V10.6C25 9.27453 23.9255 8.20001 22.6 8.20001Z"
stroke="var(--copy-icon-stroke)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.4 17.8C2.08 17.8 1 16.72 1 15.4V3.4C1 2.08 2.08 1 3.4 1H15.4C16.72 1 17.8 2.08 17.8 3.4"
stroke="var(--copy-icon-stroke)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
</defs>
</svg>

<main class="main-container">

<!-- STYLESHEET PREVIEW -->
<section class="flex-col items-start" style="gap: var(--space-15); width: var(--container-width);">
<div class="flex-col items-start" style="gap: var(--space-3);">
<h2 class="section-title">STYLESHEET</h2>
<p class="stylesheet-desc">Contains all the styles for every component on this page. Copy once and drop it into your project.</p>
</div>
<div class="flex-col items-center" style="gap: var(--space-7); width: 100%;">
<div class="css-preview">
<div class="css-preview-header">
<span class="css-preview-filename">components.css</span>
</div>
<pre class="css-preview-code"><span class="syntax-keyword">@import</span> <span class="syntax-token">url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&amp;display=swap')</span>;

<span class="syntax-comment">/* ... DESIGN block: usage guide and customization instructions ... */</span>

<span class="syntax-keyword">:root</span> {

<span class="syntax-comment">/* COLORS - BACKGROUND */</span>
<span class="syntax-token">--bg-main</span>: <span class="syntax-token">hsl(30, 26%, 84%)</span>;

<span class="syntax-comment">/* COLORS - PRIMARY ACTION */</span>
<span class="syntax-token">--primary-bg</span>: <span class="syntax-token">hsl(32, 25%, 29%)</span>;
<span class="syntax-token">--primary-text</span>: <span class="syntax-token">hsl(0, 0%, 100%)</span>;</pre>
<div class="css-preview-fade"></div>
</div>
<!--
JS hook: Instead of relying on fragile CSS classes, 'data-component' acts as a
robust bridge. It passes the exact object key (e.g., "css", "btn-primary")
to our script, allowing a single JavaScript function to handle all 16 buttons
-->
<button class="btn-copy" aria-label="Copy stylesheet" data-component="css">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</section>

<!-- SECTION 1: BUTTONS -->
<section class="flex-col items-start" style="gap: var(--space-20); width: var(--container-width);">
<h2 class="section-title">BUTTON</h2>

<div class="flex-row items-start justify-between components-row" style="width: 100%;">
<!-- Primary -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">PRIMARY</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<button class="btn-primary">Confirm</button>
<button class="btn-copy" aria-label="Copy primary button" data-component="btn-primary">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Secondary -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">SECONDARY</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<button class="btn-secondary">Review</button>
<button class="btn-copy" aria-label="Copy secondary button" data-component="btn-secondary">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Tertiary -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">TERTIARY</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<button class="btn-tertiary">Cancel</button>
<button class="btn-copy" aria-label="Copy tertiary button" data-component="btn-tertiary">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>
</div>
</section>

<!-- SECTION 2: INPUT -->
<section class="flex-col items-start" style="gap: var(--space-20); width: var(--container-width);">
<h2 class="section-title">INPUT</h2>

<div class="flex-row items-start justify-between components-row" style="width: 100%;">
<!-- Alone -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">ALONE</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<label for="input-alone" class="sr-only">Details</label>
<input type="text" id="input-alone" class="input-alone" placeholder="Add optional details...">
<button class="btn-copy" aria-label="Copy input" data-component="input">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- With Checkbox -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">WITH CHECKBOX</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="flex-col items-start" style="gap: var(--space-3);">
<label for="input-with-cbx" class="sr-only">Details</label>
<input type="text" id="input-with-cbx" class="input-alone" placeholder="Add optional details...">
<label class="flex-row items-center" style="gap: var(--space-3); cursor: pointer;">
<input type="checkbox" class="cbx-custom sr-only">
<div class="cbx-box"></div>
<span class="cbx-label">I understand this will submit</span>
</label>
</div>
<button class="btn-copy" aria-label="Copy input with checkbox" data-component="input-checkbox">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>
</div>
</section>

<!-- SECTION 3: RADIO BUTTON -->
<section class="flex-col items-start section-content" style="gap: var(--space-15);">
<h2 class="section-title">RADIO BUTTON</h2>
<div class="flex-col items-center" style="gap: var(--space-7);">
<fieldset>
<legend class="sr-only">Sync options</legend>
<div class="flex-col justify-center items-start" style="gap: var(--space-3);">
<!-- Radio 1 -->
<label class="flex-row items-center" style="gap: var(--space-3); cursor: pointer;">
<input type="radio" name="sync" class="native-radio sr-only" checked>
<svg class="custom-radio-svg" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 40 40" fill="none" aria-hidden="true">
<circle cx="20" cy="20" r="18" stroke="var(--radio-stroke)" stroke-width="4" />
<circle cx="20" cy="20" r="12" fill="var(--radio-stroke)" class="radio-dot" />
</svg>
<span class="radio-label-text">Sync automatically</span>
</label>
<!-- Radio 2 -->
<label class="flex-row items-center" style="gap: var(--space-3); cursor: pointer;">
<input type="radio" name="sync" class="native-radio sr-only">
<svg class="custom-radio-svg" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 40 40" fill="none" aria-hidden="true">
<circle cx="20" cy="20" r="18" stroke="var(--radio-stroke)" stroke-width="4" />
<circle cx="20" cy="20" r="12" fill="var(--radio-stroke)" class="radio-dot" />
</svg>
<span class="radio-label-text">Manual sync only</span>
</label>
</div>
</fieldset>
<button class="btn-copy" aria-label="Copy radio buttons" data-component="radio">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</section>

<!-- SECTION 4: CARD -->
<section class="flex-col items-start section-content" style="gap: var(--space-15);">
<h2 class="section-title">CARD</h2>

<div class="flex-col items-start cards-outer" style="gap: var(--space-10);">
<!-- Row 1 -->
<div class="flex-row items-start cards-row-1" style="gap: var(--space-8);">
<!-- Card 1 -->
<div class="flex-col items-center" style="gap: var(--space-7);">
<h3 class="component-label">1</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="card card-1" aria-hidden="true"></div>
<button class="btn-copy" aria-label="Copy card level 1" data-component="card-1">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Card 2 -->
<div class="flex-col items-center" style="gap: var(--space-7);">
<h3 class="component-label">2</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="card card-2" aria-hidden="true"></div>
<button class="btn-copy" aria-label="Copy card level 2" data-component="card-2">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Card 3 -->
<div class="flex-col items-center" style="gap: var(--space-7);">
<h3 class="component-label">3</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="card card-3" aria-hidden="true"></div>
<button class="btn-copy" aria-label="Copy card level 3" data-component="card-3">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>
</div>

<!-- Row 2 -->
<div class="cards-row-center" style="gap: var(--space-8);">
<!-- Card 4 -->
<div class="flex-col items-center" style="gap: var(--space-7);">
<h3 class="component-label">4</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="card card-4" aria-hidden="true"></div>
<button class="btn-copy" aria-label="Copy card level 4" data-component="card-4">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Card 5 -->
<div class="flex-col items-center" style="gap: var(--space-7);">
<h3 class="component-label">5</h3>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="card card-5" aria-hidden="true"></div>
<button class="btn-copy" aria-label="Copy card level 5" data-component="card-5">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>
</div>
</div>
</section>

<!-- SECTION 5: ICON -->
<section class="flex-col items-start" style="gap: var(--space-20); width: var(--container-width);">
<h2 class="section-title">ICON</h2>

<div class="flex-row items-start justify-between components-row" style="width: 100%;">
<!-- Original -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">ORIGINAL</h3>
<span class="sr-only">Shows the base icon design at 128 pixels with thin strokes, meant for large display.</span>
<div class="flex-col items-center" style="gap: var(--space-7);">
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"
fill="none" aria-hidden="true">
<rect width="128" height="128" rx="20" fill="url(#paint0_linear_40_145)" />
<path d="M77.0004 83.5006L96.5011 63.9999L77.0004 44.4993" stroke="#FFF9F2" stroke-width="8"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M50.9996 44.4993L31.4989 63.9999L50.9996 83.5006" stroke="#FFF9F2" stroke-width="8"
stroke-linecap="round" stroke-linejoin="round" />
<defs>
<linearGradient id="paint0_linear_40_145" x1="64" y1="128" x2="64" y2="0"
gradientUnits="userSpaceOnUse">
<stop stop-color="#7A6249" />
<stop offset="1" stop-color="#C29A6F" />
</linearGradient>
</defs>
</svg>
<button class="btn-copy" aria-label="Copy original icon" data-component="icon-original">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Simplified -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">SIMPLIFIED</h3>
<span class="sr-only">Shows the icon at 128 pixels but intentionally simplified with thicker internal lines to prepare for downscaling.</span>
<div class="flex-col items-center" style="gap: var(--space-7);">
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"
fill="none" aria-hidden="true">
<rect width="128" height="128" rx="20" fill="url(#paint0_linear_40_155)" />
<path d="M80.9159 89.3738L106.29 64L80.9159 38.6262" stroke="#FFF9F2" stroke-width="14"
stroke-linecap="round" stroke-linejoin="round" />
<path d="M47.0841 38.6262L21.7103 64L47.0841 89.3738" stroke="#FFF9F2" stroke-width="14"
stroke-linecap="round" stroke-linejoin="round" />
<defs>
<linearGradient id="paint0_linear_40_155" x1="64" y1="128" x2="64" y2="0"
gradientUnits="userSpaceOnUse">
<stop stop-color="#7A6249" />
<stop offset="1" stop-color="#C29A6F" />
</linearGradient>
</defs>
</svg>
<button class="btn-copy" aria-label="Copy simplified icon" data-component="icon-simplified">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>

<!-- Shrunk -->
<div class="flex-col items-center" style="gap: var(--space-15);">
<h3 class="component-label">SHRUNK</h3>
<span class="sr-only">Shows the simplified icon scaled down to 16 pixels for a favicon. The thicker lines ensure it remains perfectly legible at this tiny size.</span>
<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="icon-frame">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
fill="none" aria-hidden="true">
<rect width="16" height="16" rx="2.5" fill="url(#paint0_linear_40_165)" />
<path d="M10.1145 11.1718L13.2862 8.0001L10.1145 4.82837" stroke="#FFF9F2"
stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
<path d="M5.88553 4.82837L2.71381 8.0001L5.88553 11.1718" stroke="#FFF9F2"
stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" />
<defs>
<linearGradient id="paint0_linear_40_165" x1="8" y1="16" x2="8" y2="0"
gradientUnits="userSpaceOnUse">
<stop stop-color="#7A6249" />
<stop offset="1" stop-color="#C29A6F" />
</linearGradient>
</defs>
</svg>
</div>
<button class="btn-copy" aria-label="Copy shrunk icon" data-component="icon-shrunk">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</div>
</div>
</section>

<!-- SECTION 6: EMPTY STATE -->
<section class="flex-col items-start section-content" style="gap: var(--space-15);">
<h2 class="section-title">EMPTY STATE</h2>

<div class="flex-col items-center" style="gap: var(--space-7);">
<div class="flex-col items-start" style="gap: var(--space-11);">
<div class="flex-col items-start" style="gap: var(--space-3);">
<h3 class="title-empty">Ready when you are</h3>
<p class="subtitle-empty">Everything you create will show up here.</p>
</div>
<button class="btn-primary">New item</button>
</div>
<button class="btn-copy" aria-label="Copy empty state" data-component="empty-state">
<svg width="16" height="16" viewBox="0 0 26 26" fill="none" aria-hidden="true"><use href="#icon-copy"/></svg>
</button>
</div>
</section>

</main>

<!--
Dynamic audio feedback:
- The problem: When JavaScript injects a success message ("Copied!") into the DOM,
screen readers ignore it unless the element receives focus. A "live region"
forces the software to monitor this <div> and announce any text changes
- Polite vs. assertive: 'aria-live="polite"' tells the screen reader to finish
its current sentence before speaking. It gracefully respects the user's flow
- Why not assertive? 'aria-live="assertive"' violently interrupts and cuts off
the current audio. It should be strictly reserved for critical errors or
time-sensitive warnings (e.g. "Session expired"), never for simple success states
-->
<div id="copy-status" aria-live="polite" class="sr-only"></div>

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

</html>

The Book

The time has come to talk about the book that more than any other improved and accelerated my journey. I experienced it in two phases.

The first was when I was working in a factory: I read it in the canteen and during every available break, managing to read and study 15 pages a day, as I wrote in the project Balance Sheet, in which I found myself right in the middle of that moment. I had read 60% of it during those months. At the beginning of my journey I had drawn up a list of books to read and, even though the Code Tutor had suggested I read it later, I was too curious to wait. Reading the first part at the very beginning helped me avoid right away the classic mistakes beginners make: random spacing, equally random proportions, visual hierarchies built on font size alone instead of weight and brightness, and that conditioned reflex of making every destructive action red and threatening even when it isn't.

The second phase came now that I've left my job: in two days I finished the remaining part. I adopted the same approach as always: asking questions, going further, "pushing back" on what was being taught and letting it "push back" on me in return. I took many notes and added everything to the UX & UI Vademecum, not the one available here on the site but my private one, which unfortunately I can't publish because it would violate the intellectual property of many resources I studied from.

How the Project Came to Be

The First Attempt (Discarded)

I didn't draw inspiration from anything already existing when creating this project. For a few days I had been picturing the interface clearly in my mind: small buttons to copy the code, a copy icon not too prominent in each component so as not to disturb with its repetition, a theme that mirrored this very site. In this project the Code Tutor transformed into Claude Code Tutor: I had used it for just a few experiments before, but this is the first time I used it seriously.

The first version had been set up in a Vite + React environment. I did something I immediately regretted: once I had decided on the topics to cover, I started giving the specifications directly to Claude Code, asking it to implement everything based on the notes I had taken on the book, while still providing precise specifications on the expected result. The outcome was poor, despite several iterations:

Screenshot of the first version of the Refactoring UI case study

By this I don't mean the tool is poor: I could have used even more specific prompts. But that wasn't the only problem. I felt that in the long run I wouldn't have contributed anything at all, ending up with a project where my only input had been instructions and notes, nothing more; and, no less importantly, I was taking away from myself the most rewarding part of this work. On top of that came the complexity I perceived as utterly pointless and the repulsion I still feel toward external libraries. Above all, I couldn't find any sense in the approach, since the goal was simply to apply the most useful concepts that emerged from the reading. I could have done it in React, since I had just finished the Superhero Application Form, and therefore not starting from scratch, but I preferred to go back to basics.

I threw everything in the bin: half a day's work.

Figma First, Code After

I opened Figma and designed everything from scratch. Now that I had seen the result I didn't want, I knew exactly what I did want. I adopted the same approach I used in the web chat: an iterative process based on shared planning. Instead of writing "don't write a single line of code yet" in the chat, as I used to do before, I now wrote it in the terminal; the principle I applied was: no implementation until I had a shared and approved plan, in order to eliminate the technical debt that would inevitably have emerged from solutions not thought through from the start. I started from the Figma mockup, which is very close to the version implemented on the site. The differences you can notice concern (beyond the absence of the css preview) almost exclusively the dimensions, issues that emerged when those values were translated into CSS.

I would never have managed to do it without making that vision concrete. I made my own the authors' words:

Don't get overwhelmed working in the abstract. Build the real thing as early as possible so your imagination doesn't have to do all the heavy lifting.
~ Adam Wathan and Steve Schoger

Screenshot of the Figma version of the Refactoring UI case study

I then extracted every single property from all 15 components from Figma using Dev Mode, recording them in a .txt file with the specifications for titles, subtitles and spacing.

Claude Code

One problem I had to solve was the CSS file: how to let the user copy it entirely with a single click? So that they would be free to paste it into whichever file they wanted without being forced to start from mine? I wanted a specific component, a sort of preview at the top to copy the CSS. It's the element I contributed to only with the idea: Claude Code executed it perfectly on the first attempt. We then fixed the entire responsive side of the site. I tested it on an iPhone 15 Pro, which I recently bought to test the technical feasibility of Mosaic, an iPhone 12 mini and an Honor Magic V2 (the foldable), landscape mode included: the site is perfectly navigable even on a small phone like the 12 mini in landscape.

There were some issues during the implementation phase that opened up such interesting scenarios that I kept going deeper into the various concepts instead of moving forward. One example above all: initially I had tried to load components.css dynamically with fetch() instead of embedding it in the JS file, but I discovered two fundamental problems. The first: fetch() is blocked on the file:// protocol. The browser treats local files as untrusted origins and rejects the request. The second, more subtle: even when served normally, chaining fetch() before writeText() breaks clipboard access. The browser only allows writing to the clipboard immediately after a user click, and by the time fetch() resolves and reaches writeText(), that window has already closed. The solution was to embed everything as a template literal directly in script.js.

I also tried Antigravity as an alternative, but after 2-3 errors that rippled through the UI I went back to Claude Code. For all tasks I used Sonnet; I only used Opus for two overall checks to compare against production standards, giving me scores across various areas throughout the project, actively choosing what to fix and what to leave considering the nature of this project.

Web Chat vs Agent

Initially I thought the web chat was, at least at this stage of learning, the better choice. I was wrong. In the web chat I had additional tasks that turned into friction. With Claude Code I was in a state of total flow, and as soon as I understood that before every change I needed to have the available options listed, dissect them and understand their pros and cons before touching the code, I realised I wasn't losing anything compared to what I was doing in the web chat. I remain torn on one point though: that friction of manually copying or rewriting in my own hand the block of code from the chat, perhaps it wasn't a process that was consolidating the concept just explained. It was simply a repetitive task to which the brain no longer paid attention and carried out on autopilot, or at least that's how it seems looking back. The price I'll probably pay in the future is forgetting the syntax.

That said, I'm convinced, as I learned in Raffaele Gaito's video in which university professor Vittorio Scarano talks about responsibility in the use of AI tools, that the use of these tools will always fall back on whoever used them. No one can say "Claude made a mistake": you made a mistake because you trusted that output without understanding it, grasping it, and verifying whether more efficient and secure alternatives existed. I think this is a concept that is by no means obvious, even if in hindsight it may seem so.

Design Choices

Buttons

I decided not to go with the classic button that transitions from primary to tertiary with the same text, as shown in most educational materials, but to assign different labels: Confirm for the primary, Review for the secondary and Cancel for the tertiary. This to be more efficient in terms of message: the primary contains the call to action, the secondary with "Review" implies a button to revisit something previously written (a review, an order on an e-commerce site), while the tertiary, counterintuitively, is Cancel.

Why Cancel in the tertiary? Refactoring UI teaches that a destructive action doesn't have to be red and prominent if it isn't the primary action. If it were, then yes: red, not blinding, but with the primary style clearly visible. In cases where it isn't, it makes sense to treat it like any ordinary button. I found this concept revolutionary: in the courses I had attended (Talent Garden, Google UX), the destructive action was seen as a danger to be flagged loudly, whereas it isn't, or at least not in every case.

In the primary I added a subtle translucent white inset box-shadow on the top edge to simulate light hitting the border of a physical object rising from the page. The secondary uses a min-width slightly smaller than the primary (128px vs 140px), because I discovered that when the two appear side by side, the difference in width further accentuates the visual hierarchy between them, guiding the eye toward the main action. All without the user noticing.

Input

I used two box-shadows: an inset one coming down from the top to simulate depth, and an external one on the bottom edge that simulates the light reflection. The book argues that the input must communicate that it is an inner container where the user types - a "cavity" effect that is the physical opposite of the button, which instead rises from the screen. The WITH CHECKBOX version has the checkbox with the same proportional border-radius as the input box: 6px on 20px is 30%, versus 16px on 56px which is ~29%. The checkmark is inserted in the CSS as an SVG in data URI, with the mark color (#D5C7B8) matching exactly the input background: when the checkbox fills, the mark appears physically carved into the color.

Radio Button

I included only one type of radio button, because the goal was to communicate that native browser ones should not be used, but rather made consistent with the app's UI through a few targeted styles.

Card

I used 5 distinct shadow levels, each with two overlapping layers: a sharper one simulating direct light from above, a more diffused one simulating ambient light. The y-offset doubles at each level (1px, 2px, 4px, 8px, 16px) to create a predictable and mathematically consistent depth progression.

Icon

I included the 3 versions as Refactoring UI does, to show that the logo inside the icon needs to be "thickened" to remain recognisable at the small dimensions of a browser tab. The original icon has a stroke-width of 8px, the simplified one 14px: they therefore have the same shape but with nearly double the stroke width.

I put this principle into practice immediately after reading it, by creating the icon for this very site: the letters UX with very thick strokes to remain legible even at 16px in the browser tab.

Empty State

A simple Empty State with the primary button already created earlier, as a reminder that there must always be a call to action when there is no data to display.

Capito, li evito del tutto. Ri-traduco il documento:

What I Learned

Architecture and Accessibility

Desktop-first based on the target: I applied here the note I had written in the Telephone Number Validator: "we should evaluate based on the device that will be used predominantly". This is a UI comparison tool designed for developers in front of a large screen. Building it mobile-first would have followed the rule, but ignored the real user. I made a conscious choice to go desktop-first, handling mobile as a fallback.

Tinted greys vs neutral greys: One of the most specific concepts from the book applied in the code. Every "grey" in the project has saturation at hue 31° to harmonise with the background at 30°. A pure neutral grey (0% saturation) would appear visually disconnected from the rest of the palette, like an element imported from another UI.

Hit area without disrupting the layout: The copy icon is visually 16x16px, but the clickable area is 44x44px via padding: 14px. To keep the visual spacing unchanged, a margin-top: -14px exactly compensates for the added padding. The component occupies the same visual space as before, but is much easier to tap on mobile.

Cap-height alignment: The SVG radio is declared width="24" in the HTML as a fallback, but CSS overrides it to 20px. At 18px font-size, 20px aligns the control exactly to the cap-height of the adjacent text. This is the correct criterion for aligning inline icons with typography, not the font-size itself.

JavaScript and DOM

data-component as a robust bridge: Instead of reading which snippet to copy from the button's CSS classes, each button carries a data-component attribute that maps exactly to a key in the SNIPPETS object. HTML and JS communicate via dedicated semantic attributes, not via classes that already serve another purpose. If the CSS classes change for styling reasons, the copy logic doesn't break.

fetch() and the click window: The browser only allows writing to the clipboard immediately after a user action. fetch() is asynchronous: by the time it resolves, that window has already closed and writeText() is rejected. The solution is not to work around the browser, but to never stray from the click: the content to be copied must already be in memory at the exact moment of the click.

execCommand as a fallback: navigator.clipboard.writeText() is blocked on file:// and in some mobile Safari contexts. The fallback creates an invisible <textarea> outside the flow, selects it and triggers the system command. It's deprecated, but I discovered it to be the most reliable option in these specific edge cases.

SVG Sprite Pattern: Defining the icon once as a hidden <symbol> and referencing it everywhere via <use href="#icon-copy"> keeps the DOM clean and creates a single Source of Truth: when JavaScript replaces the SVG content to show the checkmark, it does so in one place instead of updating 16 separate elements. If the icon design changes, only one point in the code needs to be touched.

CSS and Interactions

opacity: '' instead of opacity: '1': After the copy animation, resetting svg.style.opacity = '1' leaves an inline value that silently overrides the CSS hover. The result: hover stops working after the first click, with no error in the console. Removing the inline attribute with '' returns control to the stylesheet.

pointer-events: none on the gradient fade: The gradient that fades the CSS code in the preview is in position: absolute above the text. Without pointer-events: none it would block all clicks and selections in the area below, an invisible problem that generates no error and is difficult to find without knowing where to look.

Asymmetric transitions: The transition on the base rule (the CSS rule that applies to the element at rest, with no active pseudo-classes) controls the speed of return to the normal state (160ms, fluid like a spring). The transition on :active controls the press speed (60ms, nearly instantaneous). The reason it works: CSS applies the transition from the state being left. On press, the base state is left and the 60ms kicks in. On release, :active is left and the 160ms kicks in.

display: contents in the mobile carousel: To transform the card grid into a horizontal carousel on mobile, the .cards-row-1 and .cards-row-center wrappers are dissolved with display: contents. This flattens the DOM: the children bypass their direct parent and become flex items of the carousel container. Without this, the wrappers would have broken the flex chain and disrupted the layout.

{ passive: true } on touch listeners: Adding passive: true to the touchstart and touchend event listeners explicitly tells the browser that the handler will never call preventDefault(). The browser can therefore begin scrolling immediately without waiting for the JavaScript to finish, eliminating the noticeable delay on mobile.

btn.blur() for iOS: After copying, calling btn.blur() releases the :active state on iOS, which would otherwise remain visually active until the next tap on another element. This applies both in the case of clipboard success and failure.

prefers-reduced-motion with 0.01ms: Using 0ms to zero out transitions causes a bug in some browsers: JavaScript listeners waiting for the transitionend event never fire, blocking any logic that depends on that callback. 0.01ms is short enough to be imperceptible, but long enough for the event to fire correctly.

Reflection

In the previous project on The Design of Everyday Things I had written that if a user notices one of my buttons for its aesthetics I've done graphic work, but if they click it without thinking I've done engineering work. Refactoring UI has been for me the manual of that engineering. If Norman taught me the psychology of interaction and the why of eliminating cognitive load, this book gave me the mathematics and the how to write it in CSS. My goal remains always the same: to not dazzle the eye and to free the mind. But if before I only knew that I had to answer the user's question before it was even asked, now I finally have the formulas, the tokens and the code to build that answer.


Next: Reading "Show Your Work!" by Austin Kleon, I discovered it teaches many of the patterns I used throughout this journey.