DEV Community

Sam Novak
Sam Novak

Posted on

Let the designer's spec be the test: a bounded UI change request for game UI

"The quest reward toast feels cramped and is hard to read on phones."

That's a fair complaint, and a hard thing to hand to an engineer. "Cramped" and "hard to read" are real observations, but nobody can finish them. The engineer guesses, the designer looks at the result, and the next round starts with "closer, but still not quite."

This tutorial turns that sentence into a request the engineer can finish and the designer can accept without either of them arguing about taste. The trick is small: the designer writes the spec as a data file, and a short script runs that same file as the test. I built the whole thing as a toy fixture (one HTML toast, one CSS file, the spec, and a checker run in headless Chrome) so every result below comes from an actual run.

Three versions of the quest reward toast at 360px wide: before, a first attempt that runs off the right edge and cuts off the German title, and a second attempt that fits and wraps the title

Who does what

Three roles, decided before anyone touches CSS:

  • Prepares the work: the UI designer. They know what's wrong and what "fixed" looks like. They write the intent in one sentence and the spec below.
  • Executes it: the UI engineer, in their own checkout, with their own tools. If they hand the CSS to a coding agent, that agent runs under the engineer's account and the engineer owns the diff.
  • Judges acceptance: the designer again. They read the check report and look at two screenshots. The engineer reviews the code, but doesn't decide whether the toast is right.

This split matters more once agents are involved. Human AI collaboration in game development tends to go wrong when the person who knows what "right" means isn't the one who checks the result, so the check has to be something the designer wrote.

Step 1: replace taste words with properties

Go through the complaint word by word and ask what you would measure.

  • "Cramped" becomes padding, the gap between icon and text, and icon size.
  • "Hard to read" becomes font size, line height and contrast against the toast background.
  • "On phones" becomes a viewport width (360px) and a rule that the toast stays on screen.

The designer also adds the case they worry about: the German string, which is the longest one in the localization sheet.

Step 2: write the spec as data

This is the whole template. It's the designer's file, written in the units they already use in their design tool:

{
  "change": "Quest reward toast: readable at phone width, roomier, nothing cut off",
  "requestedBy": "UI designer",
  "page": "toast.html",
  "viewports": [360, 1280],
  "checks": [
    { "name": "toast stays fully on screen with 8px to spare", "selector": ".toast", "insideViewport": 8 },
    { "name": "padding is 12px top/bottom, 16px left/right", "selector": ".toast",
      "style": { "padding-top": "12px", "padding-bottom": "12px", "padding-left": "16px", "padding-right": "16px" } },
    { "name": "12px between icon and text", "selector": ".toast", "style": { "column-gap": "12px" } },
    { "name": "icon is 40 x 40", "selector": ".toast__icon", "box": { "width": 40, "height": 40 } },
    { "name": "title is 18px on a 24px line", "selector": ".toast__title", "style": { "font-size": "18px", "line-height": "24px" } },
    { "name": "title contrast is at least 4.5:1", "selector": ".toast__title", "minContrast": 4.5, "against": ".toast" },
    { "name": "title shows all its text in at most 2 lines", "selector": ".toast__title", "maxLines": 2, "noClipping": true }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each check has a plain-language name, because the designer reads the report, not the code. The selectors are the only part the designer needs help with, and the engineer can fill those in during a two-minute call.

Note what isn't in the spec: no CSS approach, no flexbox-or-grid, no "make it pop." The spec says what must be true on screen, and the engineer chooses how.

Step 3: a checker that reads the spec

The checker is one HTML page. For each viewport it loads the component in an iframe of that exact width, then runs every check against computed styles and layout boxes. The core loop, trimmed:

for (const el of doc.querySelectorAll(c.selector)) {
  const cs = win.getComputedStyle(el), box = el.getBoundingClientRect();
  for (const [prop, want] of Object.entries(c.style ?? {})) {
    const got = cs.getPropertyValue(prop);
    if (got !== want) failures.push(`${prop} is ${got}, want ${want}`);
  }
  if (c.insideViewport !== undefined && (box.left < c.insideViewport || box.right > vw - c.insideViewport))
    failures.push(`spans ${Math.round(box.left)}-${Math.round(box.right)}px, viewport is ${vw}px`);
  if (c.maxLines && Math.round(box.height / parseFloat(cs.lineHeight)) > c.maxLines)
    failures.push(`uses more than ${c.maxLines} lines`);
  if (c.noClipping && el.scrollWidth > el.clientWidth)
    failures.push(`text is cut off (${el.scrollWidth}px of text in ${el.clientWidth}px)`);
  // box sizes and WCAG contrast work the same way
}
Enter fullscreen mode Exit fullscreen mode

A shell script serves the folder, renders the page with chrome --headless=new --dump-dom, and prints the JSON the page wrote. One detail cost me time: headless Chrome won't lay out a window narrower than its minimum, so the 360px viewport has to be an iframe inside a wider window rather than the window itself.

Step 4: the worked handoff

Before. Against the original CSS, 3 of 14 checks passed (7 checks at 2 viewports). The report said, among other things, .toast spans 16-412px, viewport is 360px, padding 6px/8px instead of 12px/16px, and title contrast 3.50:1.

Attempt 1. The engineer changed exactly the values the spec listed: padding, gap, icon, font size, line height, and a lighter title color. 11 of 14 passed. The three failures were the interesting ones:

  • At 360px the toast ran to 428px. Bigger padding on a fixed 380px content-box width made it wider than before.
  • At both widths the German title was cut off: 361px of text in 328px. The original CSS used white-space: nowrap with an ellipsis, which was harmless at 15px and clipped at 18px.

Nobody had asked for those to change, and both were caught without the designer opening a browser. Matching the listed numbers wasn't enough to fix the actual complaint.

Attempt 2. box-sizing: border-box, a width of min(380px, calc(100% - 32px)), and the title allowed to wrap instead of ellipsizing. 14 of 14 passed, with title contrast now 7.25:1.

Acceptance. The designer reads the report, then looks at the screenshot at 360px. This is where the last judgment happens, and the spec can't make it: in attempt 2 the short English title also wraps onto two lines at phone width, where it fit on one before. The spec allows that. Whether it's acceptable is the designer's call. If it isn't, that's a new check ("English title fits on one line at 360px") and a new round, not a vague "closer, but."

Limits

  • It checks what you wrote, nothing more. Seven passing checks don't mean the toast looks good. The screenshot review stays with the designer.
  • Computed styles aren't pixels. The checker would pass a toast hidden behind another element. Overlap, animation and z-order need a visual check.
  • One font, one browser. The fixture uses Arial in desktop Chrome. Real game UI with a custom font on a phone can wrap differently, so run the check where the font actually loads.
  • Contrast is only computed against a solid background. Toasts over the game scene need a worst-case background, which a static check can't pick for you.
  • Selectors are brittle. Rename a class and the check silently matches nothing. A real version should fail when a selector matches zero elements; this toy version doesn't yet.

If your studio splits work this way, with one person knowing what "done" looks like and another able to build it, Wagglet's write-up on closing team skill gaps with AI task handoffs covers the same prepare, execute, verify split for tasks beyond UI.

Top comments (0)