How Design Constraints Change AI-Generated Frontend: A Two-Version Comparison
This article reports a controlled comparison between two AI-generated versions of the same frontend page: one prompt containing only functional requirements, the other with structured design constraints added. It shows which dimensions actually improve (spacing rhythm, visual system, information hierarchy, interaction state coverage, accessibility) and which do not (requirement interpretation, slicing math, memory behavior, packaging format). It also documents the shared technical baseline both versions must get right — integer-boundary slicing, downsampling and EXIF orientation for very large images, a hand-written ZIP writer with no dependencies — plus a reusable acceptance checklist and a five-block prompt skeleton.
1. The problem: functional but unusable
When you ask an AI coding agent to generate a frontend page from scratch, the most common gap is not missing functionality. It is code that runs but is unpleasant to use. The output usually completes the main flow — upload, process, download. But the page typically has no consistent spacing rhythm, a confused type hierarchy, colors scattered across hardcoded values, only a success state and no empty, loading, or error states, no keyboard access, and controls that crush together on small screens.
The root cause is not that the model cannot write code. It is that the default optimization target is a minimal viable implementation. When the prompt describes only functionality, the model spends its limited attention budget on functional correctness, and visual consistency plus state completeness get compressed as a side effect. The value of design capability is essentially to make a whole set of implicit constraints explicit and put them into the same generation context.
So the question this experiment sets out to answer is specific: with the requirement list identical, adding structured design constraints — which dimensions of the output improve, and which stay the same? The second half of that question matters just as much. Assuming that “installing a design skill” solves everything is an easy way to fail on functionality and engineering details.
2. The test case: requirements for an online image slicing tool
To make the two versions comparable, the requirements were written as items that can be judged right or wrong. Natural-language requirements are the easiest source of variable contamination.
Hard functional requirements
- A single HTML file, usable by double-clicking or by hosting it, with no backend at all. All processing happens locally in the browser; images never leave the machine.
- Support both drag-and-drop and click-to-select upload, accept common bitmap formats, and show a clear message for non-image files.
- Three slicing modes: a 3×3 grid, a 2×2 grid, and a 1×3 vertical strip.
- Preview every piece with an index number, download individual pieces, and download everything as a single archive.
- The tool itself is the first screen: no illustrations, testimonials, statistics, or filler copy unrelated to slicing.
Non-functional requirements (required in both versions, used to expose differences)
- Every operation reachable by keyboard, visible focus styles, and interactive controls with a touch target of at least 44×44 pixels.
- A complete state matrix: initial empty state, image parsing, processing, success, single-piece failure, image too large, unsupported format.
- Viewport adaptation: usable in desktop fullscreen and in a narrow single-column layout.
- Very large images (for example 8000×6000) must not leave the page unresponsive for long or crash it.
3. Experiment protocol: making the comparison credible
- Single variable. The functional and constraint items in the two prompts are word-for-word identical. The only difference is that V2 adds a design and state constraint section.
- Same runtime environment. Same model version, same context-length band, same directory. Where randomness was a concern, two to three runs were done and the median taken.
- Fixed acceptance script. Screenshots at fixed viewports, with the state matrix and functional items checked off one by one, rather than a general impression.
- Scorecard (1–5, at least two independent reviewers). Functional completeness, visual system consistency, information hierarchy, interaction state completeness, accessibility, responsive behavior, code maintainability, artifact size and dependency count.
- Functional test cases first, then scoring. Verify “does it actually process the image correctly” before looking at how it looks. The reverse order makes it easy for a pretty interface to hide functional defects.
4. V1 (no design capability): typical results
Functionally, this version usually passes: upload, slicing, and single-piece download mostly work, and bulk download may be implemented rather sloppily. The problems cluster in four places.
- No visual system. Spacing mixes 4/6/10/13px, corner radii and shadow styles are inconsistent, body and heading sizes come from visual intuition rather than a scale, and colors appear as inline styles and magic values.
- Missing states. Only the success path exists. The empty state is a single line of text, the loading state is missing so the page looks frozen while a large image is processed, and the error state is usually an
alert. - Accessibility gaps. Icon buttons have no accessible name, focus is removed by
outline: none, selection is conveyed by color alone, and contrast fails. - Engineering leaks. This is the most overlooked part, and the one that most affects real usability: for example, rounding loss at slice edges, decoding a very large image in one pass and blowing up memory, not applying EXIF orientation so slices do not match what the preview showed, and object URLs that are never revoked.
5. V2 (structured design constraints injected): where the difference shows up
The differences appear on three levels.
First, design tokens come first. The model was asked to define four variable groups — spacing, color, radius, font — before writing layout, and the page gains rhythm as a result. Colors get a semantic division of labor (surface, border, primary action, destructive action, three text levels), font sizes form a scale by fixed ratio, and spacing is drawn only from a fixed set of steps.
Second, the state matrix gets enumerated. Explicitly requiring “an interface for every state” makes the model proactively add skeleton placeholders, progress feedback, retry on failure, and a degraded notice for oversized images. None of that appears spontaneously in the unconstrained version.
Third, a self-check step is built in. Requiring the model to review its output against a checklist (contrast, visible focus, keyboard order, narrow-screen layout) moves review earlier and reduces rework later.
But the boundary needs to be stated clearly: design constraints barely improve requirement-understanding correctness or low-level algorithm correctness. Whether slicing loses an edge, whether large images are downsampled, and whether the archive format is valid depend on engineering constraints and acceptance test cases, not on design capability. Conversely, if design enhancements come with heavy dependencies, they push up artifact size and can conflict with the hard requirement of a single file that opens offline — at which point you should proactively downgrade to native implementations instead of keeping the dependency. That is the real conclusion of the comparison: design capability delivers completeness and consistency, engineering discipline delivers correctness and maintainability, and neither substitutes for the other.
6. The technical foundation both versions must get right
6.1 Slice coordinates and rounding
Slice on integer boundaries so accumulated error does not drop pixels from the last row or column. When rows and columns are fixed, compute block size with Math.floor; the remaining margin is either folded into the last block or discarded, but the strategy must be documented.
function blockRect(width, height, rows, cols, r, c) {const bw = Math.floor(width / cols);const bh = Math.floor(height / rows);const isLastCol = c === cols - 1;const isLastRow = r === rows - 1;const x = c * bw;const y = r * bh;// Fold the remainder into the last block so the pieces add up exactlyconst w = isLastCol ? width - x : bw;const h = isLastRow ? height - y : bh;return { x, y, w, h };}
Draw with an offscreen canvas in batches, output a consistent format (PNG for fidelity; fall back to high-quality JPEG when size is constrained), and create an object URL for each Blob right away for preview, calling URL.revokeObjectURL on replace or unmount.
async function sliceBitmap(bmp, rows, cols) {const out = [];for (let r = 0; r < rows; r++) {for (let c = 0; c < cols; c++) {const { x, y, w, h } = blockRect(bmp.width, bmp.height, rows, cols, r, c);const canvas = document.createElement('canvas');canvas.width = w;canvas.height = h;canvas.getContext('2d').drawImage(bmp, x, y, w, h, 0, 0, w, h);out.push(await new Promise(res => canvas.toBlob(res, 'image/png')));}}return out;}
6.2 Very large images and orientation
Decoding an oversized image is the main source of the memory peak. The reasonable approach is to downsample proportionally first with a bitmap decode that takes target dimensions (only when a longest-edge cap is needed), then enter the slicing flow. Also state in the UI that “images whose longest edge exceeded the threshold were automatically scaled to N pixels,” so users do not think they are getting slices of the original. Additionally, some camera photos carry an orientation flag; decode with the orientation option honored, otherwise the rotation of the slices will not match the preview.
// Cap the longest edge at 6000px, downsample with high quality, and honor the file's own orientationconst bmp = await createImageBitmap(file, {resizeWidth: scaledW,resizeHeight: scaledH,resizeQuality: 'high',imageOrientation: 'from-image'});
Where offscreen canvas is unavailable in older environments, fall back to a regular canvas element; the logic is identical.
6.3 Packaging and downloading entirely in the frontend
“Bulk download” needs neither a backend nor a packaging library. A ZIP archive can be written by hand using the stored (uncompressed) method: local file header + data + central directory + end of central directory record, four parts concatenated into a Blob. The images are already compressed formats, so re-compressing them buys almost nothing, and stored mode is the simplest option.
const crcTable = (() => {const t = new Uint32Array(256);for (let i = 0; i < 256; i++) {let c = i;for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);t[i] = c >>> 0;}return t;})();function crc32(bytes) {let c = 0xFFFFFFFF;for (let i = 0; i < bytes.length; i++) c = crcTable[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8);return (c ^ 0xFFFFFFFF) >>> 0;}function makeZip(files) { // files: [{ name, data: Uint8Array }]const enc = new TextEncoder();const locals = [], centrals = [];let offset = 0;for (const f of files) {const name = enc.encode(f.name);const crc = crc32(f.data);const lh = new DataView(new ArrayBuffer(30));lh.setUint32(0, 0x04034b50, true);lh.setUint16(4, 20, true); // version needed to extractlh.setUint16(6, 0x0800, true); // UTF-8 filenamelh.setUint16(8, 0, true); // stored, no compressionlh.setUint32(14, crc, true);lh.setUint32(18, f.data.length, true);lh.setUint32(22, f.data.length, true);lh.setUint16(26, name.length, true);lh.setUint16(28, 0, true);const ch = new DataView(new ArrayBuffer(46));ch.setUint32(0, 0x02014b50, true);ch.setUint16(4, 20, true);ch.setUint16(6, 20, true);ch.setUint16(8, 0x0800, true);ch.setUint16(10, 0, true);ch.setUint32(16, crc, true);ch.setUint32(20, f.data.length, true);ch.setUint32(24, f.data.length, true);ch.setUint16(28, name.length, true);ch.setUint32(42, offset, true); // local header offsetlocals.push(new Uint8Array(lh.buffer), name, f.data);centrals.push(new Uint8Array(ch.buffer), name);offset += 30 + name.length + f.data.length;}const cdSize = centrals.reduce((s, u) => s + u.length, 0);const end = new DataView(new ArrayBuffer(22));end.setUint32(0, 0x06054b50, true);end.setUint16(8, files.length, true);end.setUint16(10, files.length, true);end.setUint32(12, cdSize, true);end.setUint32(16, offset, true);return new Blob([...locals, ...centrals, new Uint8Array(end.buffer)],{ type: 'application/zip' });}
6.4 Performance and stability
- Generate Blobs block by block rather than holding nine large canvases at once; release the bitmap reference when processing finishes.
- Show progress feedback before processing a large image (chunked, asynchronous, yielding the main thread) so the click does not feel dead.
- A single block failing must not abort the whole run; record the failed index and allow a retry.
- Zero uploads is both a privacy selling point and a compliance statement: put that copy where it is visible on the first screen, not buried in the footer.
7. Turning “design capability” into a reusable checklist
Whether or not design enhancement is used, this list should be part of the acceptance criteria:
- Spacing scale: use only multiples of 4 (4/8/12/16/24/32); no ad-hoc 13px.
- Color tokens: distinguish surface, border, primary action, destructive action, and three text levels; no raw color values inside components.
- Type scale: body 15–16px, headings stepping up by a fixed ratio, no stacked font weights within the same level.
- Contrast: at least 4.5:1 for body text, at least 3:1 for large text; selection and error states must not rely on color alone and need text or an icon.
- Focus and keyboard: the focus ring is always visible; the operation order is upload → slicing mode → single-piece download → download all.
- State matrix: empty, parsing, processing, success, single-piece failure, image too large, unsupported format — each with an interface.
- Dependency budget: when a single offline-capable file is required, prefer native implementations; any new dependency needs a stated reason.
8. A five-block prompt skeleton (reusable as-is)
1) Capability inventory: first list the capabilities/rules you currently have for web design and UI optimization;if any are missing, say what should be added and what it costs in size or dependencies, and ask me before starting.2) Hard functional constraints: single file, purely local processing, drag-and-drop plus click upload, three slicingmodes, preview with index numbers, single-piece download, bulk download as an archive, tool itself on first screen.3) Non-functional constraints: full keyboard access, visible focus, touch targets >= 44px, complete state matrix,narrow single-column layout, explicit degradation strategy for very large images.4) Design constraints: output the four token groups (spacing / color / radius / font) before writing layout;take spacing only from the fixed scale; meet contrast and type-scale requirements; restrained motion used only for feedback.5) Acceptance and delivery: self-test each functional case and list the results, output the file manifest and how to openit locally; for items that failed, state the cause and the fix plan. Do not gloss over them.
The key change in block 1 is ask about the cost before starting. Having the tool evaluate size and offline compatibility before it enhances its capability is cheaper than cleaning up afterwards.
9. Shipping at essentially zero cost
A purely static single page is a natural fit for static website hosting on object storage, fronted by a CDN. No server and no database, so the cost is minimal. The essentials:
- Hash filenames by content before upload and cache them for a long time (
immutable); keep the entry HTML on a short cache so iteration stays fast. - Force HTTPS, which also avoids browser restrictions on downloads and clipboard access.
- Keep the bundle in the hundreds of KB so the first screen renders without waiting on a network request.
- If you serve it from Baidu AI Cloud object storage with a CDN, enabling static website hosting plus cache rules is enough to go live. With no backend at any point, there are no additional compute or database charges.
10. Conclusion
The net result of the comparison: design constraints measurably improve interface consistency, hierarchy, and state completeness, moving a “usable prototype” to a “tool you can hand to someone else.” They will not get slice rounding, memory peaks, or archive format right for you. The reliable approach is to put both kinds of constraint into the prompt — design constraints buy completeness, engineering constraints and acceptance cases buy correctness — and to subtract deliberately when dependency and offline requirements collide.
