Ask an image model for "a clean modern landing page hero for a SaaS app" and you'll get something that looks fine. Soft lavender gradient. A floating dashboard tilted 8 degrees. Three feature icons in rounded squares. A violet "Get started free" button. You've seen it a thousand times, because you literally have: it's the average of every SaaS hero the model was trained on.
I've been a designer for more than 30 years. For the last few I've been watching developers fight this output, and the fix isn't a better model or a magic keyword. It's the same thing I'd tell a junior designer on day one: stop describing feelings and start specifying decisions.
So I turned the rules I actually use into constraints a model can follow, wrote a small function that assembles them into a prompt, and tested it against vague prompts on the same model. Everything below is copy-paste: the constraint blocks, a system prompt, Python and JavaScript builders, a lint checklist, and the exact prompts behind each example.
Why models default to the average
Two mechanics explain most "AI slop."
1. Vague adjectives resolve to the statistical mean. "Clean," "modern," "professional," and "premium" don't point to a visual. They point to a cloud of millions of images that were tagged or captioned with those words. When the model has nowhere specific to land, it lands in the middle of that cloud. The middle of everything looks like nothing in particular. That's the generic look.
Think of it like an underspecified function call. If you pass render(style="modern"), every unset parameter falls back to a default, and the defaults are whatever was most common in training. A designer's job is mostly setting those parameters on purpose.
2. The model's resting state is "add more." Unconstrained, image models reach for gradients, drop shadows, glow, texture overlays, decorative icons, stock people, and full-saturation color. Each of those feels like it's adding value one at a time. Put together they're noise. Restraint is never the default. You have to ask for it, every time, by name.
So the fix has two parts: replace adjectives with measurable facts (ratios, hex codes, positions, counts), and always ship an exclusion list.
The six constraints: IMPACT
I group the rules under six letters, IMPACT. Every layout problem I've diagnosed in three decades falls under one of them. Each block below is written in prompt language, so you can paste it straight into a prompt and edit the values.
I: Importance (reading order)
"Make the headline bigger" is a vibe. A ratio is a spec. Name what reads first, second, third, and what must stay quiet.
IMPORTANCE: reading order is headline, then subhead, then CTA button, then product screenshot. The headline is at least 3x the size of the subhead. Everything else stays quiet. A 3:1 ratio works for most screens. Posters can go 4:1 or more. Below roughly 2:1, the model tends to make everything the same "medium-important" size, which is the main reason generated layouts feel flat.
M: Margins (space)
Models and beginners both under-space. Open space reads as confidence. Ask for it as a number, and differentiate spacing inside a group from spacing between groups (that's proximity, and it's how readers know what belongs together).
MARGINS: at least 10% of the width on every side; generous negative space. Tight spacing inside groups, open space between groups. P: Palette (roles, not just colors)
Listing colors isn't enough. "Navy, coral, gray" lets the model spread all three everywhere. Assign each color a job, roughly 60/30/10: anchor (background and big areas), support (text and structure), accent (the one thing you want found). Three colors is the ceiling. Only the accent is saturated.
PALETTE (60/30/10): anchor #F8F7F4 used for background; support #2B2B2B used for all text; accent #3D5A80 used for the single CTA button. Only the accent is saturated; it appears once or twice. If you have no brand colors, these three sets are safe starting points:
| Mood | Anchor | Support | Accent |
|---|---|---|---|
| Calm / professional |
|
|
|
| Warm / approachable |
|
|
|
| Minimal |
|
|
|
A: Alignment
Almost-aligned is worse than unaligned. People can't name it, but they feel it as sloppiness. Pick one rule and say it. Left-aligned usually carries more authority than centering everything.
ALIGNMENT: all text flush left to a single margin. Every element snaps to one grid. For a two-part layout: asymmetric: text block flush left in the left half, product screenshot in the right half.
C: Contrast
Contrast is the difference that makes the important thing findable: size, weight, color, space. One strong contrast decision does more than five weak ones. The test I use is the squint test. Blur the image. If the dominant element disappears, contrast is broken.
CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid. (For real UI, still check WCAG: 4.5:1 for body text, 3:1 for large text. The model won't do it for you.)
T: Typography
Two typefaces maximum. One family in two weights is often better. Image models render named fonts poorly, so for image generation describe the character of the type; for HTML or code output, name actual fonts.
TYPOGRAPHY: bold grotesque sans-serif for headlines, regular-weight grotesque sans-serif for everything else. No other fonts. For code output: Headlines in Inter 700, body in Inter 400 at 16px minimum, headline ≈ 3x body. Reliable free pairs: Playfair Display + Inter, Merriweather + Open Sans, Lora + Lato.
The seventh block: exclusions
Not a letter, but non-negotiable. Pick the four to eight that fit the medium.
AVOID: gradients, drop shadows, glow or lens flare, texture or noise overlays, decorative borders, stock-photo people, script or display fonts. Replace these words before they reach the model
A quick translation table I run on every brief:
| You wrote | Write instead |
|---|---|
| clean, minimal | single flat background, generous whitespace, two colors max, no ornament |
| modern | name an era: Swiss International Style, contemporary Scandinavian minimal |
| professional | restrained palette, left-aligned, one accent, no decoration |
| premium, luxury | fewer elements, more whitespace, quieter color (asking for "luxury" gets gold gradients) |
| bold | heavy weight, large size jump, one dominant element |
| make it pop | name the mechanism: larger, higher contrast, or the only saturated element |
A copy-paste system prompt
If you're wiring an LLM in front of an image model (or asking an LLM to write HTML/CSS), put this in the system prompt. It turns any user brief into an IMPACT-constrained prompt.
You are a senior graphic designer converting design briefs into generation prompts. Rules: 1. Never pass vague adjectives (clean, modern, professional, premium, elevated, pop) to the output. Translate each into visible facts: color, size, position, count. 2. Output one prompt in this order: subject + format + exact dimensions; style reference (an era, medium, or genre, not an adjective); then six labeled lines: IMPORTANCE: explicit reading order and a size ratio (default headline >= 3x next level). MARGINS: minimum margin as % of width; tight within groups, open between groups. PALETTE: max 3 hex colors, each with a role (anchor ~60%, support ~30%, accent ~10%). Only the accent is saturated. ALIGNMENT: one alignment rule; every element snaps to one grid. CONTRAST: one dominant element; dark on light or light on dark. TYPOGRAPHY: max 2 typefaces. Named fonts for code output; type character for images. 3. Quote every word of on-image text exactly and add "no other words." 4. End with "AVOID:" and 4-8 exclusions suited to the medium. 5. If the brief lacks brand colors, fonts, or copy, choose sensible defaults and list them as assumptions after the prompt. Never invent brand facts the user did supply. 6. Pin structure, leave one or two decisions open. Do not specify every pixel. The code: build the prompt from parameters
Hand-writing that block every time is error-prone, and the constraints are really data. So I made them data. This is a single file, standard library only. An optional graphic field lets you name the one strong visual idea, and an explicit "No other shapes" stops the model adding a second. It also enforces two rules (three colors, two typefaces) by raising, so a bad spec fails fast instead of producing a muddy image.
from dataclasses import dataclass, field DEFAULT_EXCLUSIONS = ["gradients", "drop shadows", "glow or lens flare", "texture or noise overlays", "decorative borders", "stock-photo people", "script or display fonts"] @dataclass class ImpactSpec: format: str # "SaaS landing page hero, desktop" dims: str # "1536x1024 px" style: str # a visual territory, not an adjective palette: dict # {"anchor": ("#F8F7F4", "background"), ...} type_pair: tuple # ("serif headline", "sans-serif body") hierarchy: list # reading order, first item dominates hierarchy_ratio: float = 3.0 # headline : next level alignment: str = "all text flush left to a single margin" margins: str = "at least 10% of the width on every side; generous negative space" graphic: str = "" # optional: ONE strong graphic element text: dict = field(default_factory=dict) # exact copy, verbatim exclusions: list = field(default_factory=lambda: list(DEFAULT_EXCLUSIONS)) def build_prompt(s: ImpactSpec) -> str: if len(s.palette) > 3: raise ValueError("Palette: three colors max (anchor/support/accent).") if len(s.type_pair) > 2: raise ValueError("Typography: two typefaces max.") colors = "; ".join(f"{role} {hex_} used for {job}" for role, (hex_, job) in s.palette.items()) order = ", then ".join(s.hierarchy) copy = " ".join(f'{k}: "{v}".' for k, v in s.text.items()) return "\n".join([ f"Create a {s.format}, {s.dims}. Style: {s.style}.", f"IMPORTANCE: reading order is {order}. The {s.hierarchy[0]} is at least " f"{s.hierarchy_ratio:g}x the size of the {s.hierarchy[1]}. Everything else stays quiet.", f"MARGINS: {s.margins}. Tight spacing inside groups, open space between groups.", f"PALETTE (60/30/10): {colors}. Only the accent is saturated; it appears once or twice.", f"ALIGNMENT: {s.alignment}. Every element snaps to one grid.", f"GRAPHIC: exactly one graphic element: {s.graphic}. No other shapes, icons or images." if s.graphic else "", f"CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid.", f"TYPOGRAPHY: {s.type_pair[0]} for headlines, {s.type_pair[1]} for everything else. No other fonts.", f"TEXT (render exactly, no other words): {copy}" if copy else "", "AVOID: " + ", ".join(s.exclusions) + ".", ]).replace("\n\n", "\n").strip() if __name__ == "__main__": print(build_prompt(ImpactSpec( format="SaaS landing page hero section, desktop browser", dims="1536x1024 px", style="Swiss International Style, contemporary product site, flat", palette={"anchor": ("#F8F7F4", "background"), "support": ("#2B2B2B", "all text"), "accent": ("#3D5A80", "the single CTA button")}, type_pair=("bold grotesque sans-serif", "regular-weight grotesque sans-serif"), hierarchy=["headline", "subhead", "CTA button", "product screenshot"], text={"Headline": "Ship invoices in one click", "Subhead": "Billing for teams that hate billing.", "Button": "Start free"}, ))) Run it and you get:
Create a SaaS landing page hero section, desktop browser, 1536x1024 px. Style: Swiss International Style, contemporary product site, flat. IMPORTANCE: reading order is headline, then subhead, then CTA button, then product screenshot. The headline is at least 3x the size of the subhead. Everything else stays quiet. MARGINS: at least 10% of the width on every side; generous negative space. Tight spacing inside groups, open space between groups. PALETTE (60/30/10): anchor #F8F7F4 used for background; support #2B2B2B used for all text; accent #3D5A80 used for the single CTA button. Only the accent is saturated; it appears once or twice. ALIGNMENT: all text flush left to a single margin. Every element snaps to one grid. CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid. TYPOGRAPHY: bold grotesque sans-serif for headlines, regular-weight grotesque sans-serif for everything else. No other fonts. TEXT (render exactly, no other words): Headline: "Ship invoices in one click". Subhead: "Billing for teams that hate billing.". Button: "Start free". AVOID: gradients, drop shadows, glow or lens flare, texture or noise overlays, decorative borders, stock-photo people, script or display fonts. Same thing in JavaScript, for a Node build step or a Next.js route:
function buildPrompt({ format, dims, style, palette, typePair, hierarchy, ratio = 3, alignment = "all text flush left to a single margin", graphic = "", text = {}, exclusions = ["gradients", "drop shadows", "glow", "texture overlays", "decorative borders"] }) { if (Object.keys(palette).length > 3) throw new Error("Palette: three colors max"); const colors = Object.entries(palette).map(([r, [hex, job]]) => `${r} ${hex} used for ${job}`).join("; "); const copy = Object.entries(text).map(([k, v]) => `${k}: "${v}".`).join(" "); return [ `Create a ${format}, ${dims}. Style: ${style}.`, `IMPORTANCE: reading order is ${hierarchy.join(", then ")}. The ${hierarchy[0]} is at least ${ratio}x the size of the ${hierarchy[1]}.`, `MARGINS: at least 10% of the width on every side; generous negative space.`, `PALETTE (60/30/10): ${colors}. Only the accent is saturated.`, `ALIGNMENT: ${alignment}. Every element snaps to one grid.`, graphic && `GRAPHIC: exactly one graphic element: ${graphic}. No other shapes, icons or images.`, `CONTRAST: one dominant element; dark on light or light on dark, never mid on mid.`, `TYPOGRAPHY: ${typePair[0]} for headlines, ${typePair[1]} for everything else. No other fonts.`, copy && `TEXT (render exactly, no other words): ${copy}`, `AVOID: ${exclusions.join(", ")}.`, ].filter(Boolean).join("\n"); } module.exports = { buildPrompt }; if (require.main === module) console.log(buildPrompt({ format: "square social post", dims: "1080x1080 px", style: "contemporary Scandinavian minimal", palette: { anchor: ["#FDF6EC", "background"], support: ["#3B2F2F", "all text"], accent: ["#C97B63", "one underline"] }, typePair: ["high-contrast serif", "clean sans-serif"], hierarchy: ["headline", "description"], text: { Headline: "Version 2.0 is here" } })); Because the spec is a plain object, you can store brand presets in JSON, version them in git, and diff them in code review. Changing the accent color for a campaign becomes a one-line PR instead of a re-write of a paragraph of adjectives.
Lint your output
Generation isn't the end. I run every result through the same six lenses, and the failure tells me which line of the spec to change. Fix structure (I, M, A) before surface (P, C, T).
- [ ] Importance: Squint. Can you name what reads first within one second? Is the ratio visibly about what you specified?
- [ ] Margins: Is there at least the requested margin on every edge? Does anything touch the edge that shouldn't?
- [ ] Palette: Count distinct colors (an eyedropper or a quick k-means helps). More than three plus tints? Is the accent the only saturated color?
- [ ] Alignment: Draw a vertical line at the left edge of the text. Does every text block start on it?
- [ ] Contrast: Any mid-gray text on a mid-tone background? For UI, run a WCAG checker.
- [ ] Typography: More than two typefaces? Is every quoted string spelled exactly as given? Any extra words the model invented?
- [ ] Exclusions: Did a gradient, shadow, or glow sneak back in? Add it to
exclusionsby name and regenerate.
When something fails, don't rewrite the whole prompt. Change the one parameter (raise hierarchy_ratio, tighten exclusions, narrow the accent's job) and regenerate. That's what makes this debuggable instead of a slot machine.
Before and after: same model, same settings
All images below came from the same image model with the same settings. The only variable is the prompt. The "after" prompts are the literal output of build_prompt(); the spec objects are shown so you can reproduce them.
1. SaaS landing page hero
Before prompt:
a clean modern landing page hero for a SaaS app After prompt (generated by build_prompt):
Create a SaaS landing page hero section, desktop browser, 1536x1024 px. Style: Swiss International Style, contemporary product site, flat. IMPORTANCE: reading order is headline, then subhead, then CTA button, then product screenshot. The headline is at least 3x the size of the subhead. Everything else stays quiet. MARGINS: at least 10% of the width on every side; generous negative space. Tight spacing inside groups, open space between groups. PALETTE (60/30/10): anchor #F8F7F4 used for background; support #2B2B2B used for all text and the screenshot outline; accent #3D5A80 used for the single CTA button. Only the accent is saturated; it appears once or twice. ALIGNMENT: asymmetric: text block flush left in the left half, simple flat product screenshot in the right half. Every element snaps to one grid. CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid. TYPOGRAPHY: bold grotesque sans-serif for headlines, regular-weight grotesque sans-serif for everything else. No other fonts. TEXT (render exactly, no other words): Headline: "Ship invoices in one click". Subhead: "Billing for teams that hate billing.". Button: "Start free". AVOID: gradients, drop shadows, glow or lens flare, texture or noise overlays, decorative borders, stock-photo people, script or display fonts. 
The "before" isn't bad. That's the point. It's competent and indistinguishable: gradient wash, tilted dashboard, floating stat cards, a feature row nobody asked for, and copy the model made up. The "after" has one headline at a clear 3:1 ratio, one accent color used exactly once (the button), a flush-left text block, and the exact words I specified. It looks like a decision was made.
2. Tech meetup poster
Before prompt:
a professional poster for a tech meetup After prompt:
Create a portrait print poster for a developer meetup, 18x24 in at 300 DPI. Style: 1970s Swiss editorial poster, screen-printed flat color, one large geometric shape. IMPORTANCE: reading order is event title, then date and venue, then small URL line. The event title is at least 4x the size of the date and venue. Everything else stays quiet. MARGINS: at least 10% of the width on every side; generous negative space. Tight spacing inside groups, open space between groups. PALETTE (60/30/10): anchor #F2F2F2 used for background; support #1A1A1A used for all text; accent #E4572E used for one large circle in the upper right. Only the accent is saturated; it appears once or twice. ALIGNMENT: all text flush left to a single margin. Every element snaps to one grid. CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid. TYPOGRAPHY: heavy condensed sans-serif for headlines, regular sans-serif for everything else. No other fonts. TEXT (render exactly, no other words): Title: "Rust Night". Details: "Thursday, Nov 12 · 7 PM · Pier 70, San Francisco". URL: "rustnight.dev". AVOID: gradients, drop shadows, glow or lens flare, texture or noise overlays, decorative borders, stock-photo people, script or display fonts. 
"Professional" got neon, a blue-to-magenta gradient on the title, four icon bullets, a stock crowd photo, a neon sign, and invented dates and venue. Seven competing focal points. The IMPACT version has one: the title at roughly 4x the details, one orange circle as the only saturated element, and the venue line exactly as written. You can read it from across a room, which is the only job a poster has.
3. Developer conference Instagram post
Before prompt:
a clean modern Instagram post announcing a developer conference, in English After prompt:
Create a square Instagram post announcing a developer conference, 1080x1080 px. Style: contemporary Swiss-inspired tech editorial, flat screen-printed color, bold and graphic. IMPORTANCE: reading order is headline, then one-line description, then small URL. The headline is at least 3x the size of the one-line description. Everything else stays quiet. MARGINS: at least 10% of the width on every side; generous negative space. Tight spacing inside groups, open space between groups. PALETTE (60/30/10): anchor #FDF6EC used for background; support #3B2F2F used for all text; accent #C97B63 used for the oversized angle-bracket graphic. Only the accent is saturated; it appears once or twice. ALIGNMENT: all text flush left to a single margin. Every element snaps to one grid. GRAPHIC: exactly one graphic element: oversized terracotta code angle brackets '</>' drawn as thick flat geometric strokes, filling the right two-thirds of the canvas and bleeding off the right edge, with the headline overlapping the left bracket. No other shapes, icons or images. CONTRAST: one dominant element; text is dark on light or light on dark, never mid on mid. TYPOGRAPHY: heavy condensed grotesque sans-serif in black weight, tightly tracked for headlines, regular-weight grotesque sans-serif for everything else. No other fonts. TEXT (render exactly, no other words): Headline: "DevCon 2027". Description: "March 4–5 · Seattle · 40 talks, zero keynotes about synergy". URL: "devcon.io". AVOID: gradients, drop shadows, glow or lens flare, texture or noise overlays, decorative borders, stock-photo people, script or display fonts. 
The "before" is the conference post you scrolled past this morning: dark navy, a blue-to-purple gradient on the title, a stage photo in a tilted frame, four icon features, a gradient ticket button, and a name, dates, and venue the model made up. Every element is shouting, so nothing is. The constrained version uses the optional graphic parameter to allow exactly one bold element: oversized terracotta </> brackets that bleed off the edge and tuck behind a heavy condensed headline. It's louder than the "before" in the one place that matters and silent everywhere else. One accent color, flush-left text, the exact words I wrote, and it still reads at thumbnail size.
Note the restraint isn't "make it plain." A minimal prompt with no focal graphic gave me a correct but bland card. Constraints remove the noise; you still have to specify the one strong idea.
What this doesn't solve
To be straight about limits: image models still misspell text sometimes, so check every string. They also drift on exact hex values, so treat the palette as a strong steer and correct in post if brand color matters. And over-constraining has a cost. If you pin every element's position you get stiff, mechanical layouts. Pin the structure (what dominates, where the main blocks live, what's excluded) and leave a decision or two to the model.
But the core finding has held across every model I've tried. The gap between generic and designed output is mostly not the model. It's the number of decisions you made before you hit enter. Turn those decisions into parameters and you can make them once, reuse them, and review them like any other code.
James Shehan is a designer with 30+ years of experience and the author of Prompt Like a Designer.

