UI asset sourcing
A design direction with no assets becomes a wireframe with lorem ipsum and
grey boxes. Filling those boxes is where two specific, very visible failures
happen:
- Invented SVG path data. A model asked for "a Stripe logo" or "a
calendar icon" will confidently emit a
<path d="M12 2L2 7..."> that
renders as an unrecognizable blob. Path data cannot be recalled correctly.
- Emoji standing in for icons. 🚀 in a feature card is the single fastest
tell of generated UI, and it breaks cross-platform (rendering differs per
OS) and for screen readers.
Both are avoidable by fetching rather than recalling. That is most of what
this skill is.
Icons
Pick one set and stay in it. Mixing sets is visible immediately — stroke
weights and corner radii will not match, and the interface reads as assembled
rather than designed.
| Set |
Fits |
Notes |
| Lucide |
Most product UI; the safe default |
~1500 icons, consistent 24×24 / 2px stroke, MIT |
| Heroicons |
Tailwind projects |
Outline + solid pairs, by Tailwind's authors, MIT |
| Phosphor |
When you need weight variation |
6 weights including duotone, flexible, MIT |
| Tabler |
Dense/data UI needing breadth |
~5000 icons, 24×24 / 2px stroke, MIT |
| Material Symbols |
Android/Material products |
Variable axes (weight, fill, grade), Apache 2.0 |
| Simple Icons |
Brand/company logos only |
Official marks — the correct source when you need a real logo |
How to get the actual SVG — never write path data from memory:
- Ask the user to install the package (
lucide-react, @heroicons/react,
@phosphor-icons/react, …) and import icons by name. This is the right
answer for almost every real project: the names are memorable and correct,
the path data comes from the package.
- If a raw SVG file is genuinely needed and you have network access,
scripts/fetch_icon.py pulls it from the registry's actual published
source (Lucide/Heroicons/Tabler/Phosphor/Simple Icons — see Automated
Fetching below) instead of hand-writing it — this is what closes the loop
asset_lint.py's invented_svg_path check can otherwise only catch after
the fact.
- If you do not have network access, say so and give the import line
instead — a stated limitation is better than a plausible-looking wrong path.
Automated Fetching
python scripts/fetch_icon.py --library lucide --name arrow-right # prints real SVG to stdout
python scripts/fetch_icon.py --library lucide --name arrow-right --out a.svg
python scripts/fetch_icon.py --list-libraries
python scripts/fetch_icon.py --demo # offline, no network needed
Fetches from each registry's real published URL (unpkg.com/raw.githubusercontent.com
for Lucide/Heroicons/Tabler/Phosphor/Simple Icons — see the script header for
the exact templates) and validates the response actually parses as SVG with a
real shape element before accepting it — several of these CDNs return HTTP 200
with an HTML or plain-text error body for a bad name, so status code alone
isn't proof. Fails loud with the resolved URL on a miss rather than writing
garbage to disk. --demo proves the fetch/validate logic offline by injecting
fixed responses (valid SVG, a disguised 404 page, an empty body) — it does not
require network access, but real usage does; --list-libraries shows every
registry's URL template without fetching anything.
Deliberately does not vendor icon files into this repository — a copy here
would silently go stale the moment the upstream registry updates, the same
"quietly wrong forever" failure this catalog's other data-integrity gates
exist to catch. Fetch at generation time instead of storing a copy.
Consistency rules that matter more than the set you picked:
- One nominal size (usually 24×24) with a fixed stroke width across the whole
UI; scale via the container, not by redrawing at another weight.
- Icons that carry meaning need an accessible name (
aria-label, or adjacent
text). Decorative icons get aria-hidden="true" so they are not announced.
- An icon alone is rarely enough for a primary action — pair with a label
unless the metaphor is universally understood (close, search, back).
- Match the icon style to the aesthetic: outline icons for minimal/Swiss,
filled/duotone for playful/claymorphic, sharp geometric for brutalist. A
rounded friendly icon set inside a brutalist layout fights the direction.
Imagery
Decide first what kind of imagery the direction calls for — see
references/imagery_direction.md for the per-category guidance (photography,
illustration, abstract/gradient, 3D, or deliberately none). The wrong kind
is a bigger error than a mediocre execution of the right kind.
When to generate rather than source stock:
Generate when the image must sit inside the palette you already chose, when
the subject is specific to this product, or when stock would read as
generic-corporate (the smiling-team-around-a-laptop problem). Aria Code has
two backends, both MCP-exposed:
aria.report.generate_image_local — self-hosted SDXL-Turbo. No API key, no
per-call cost. Good for texture, abstract backgrounds, and light restyling.
Weak at aggressive restyling and cannot render legible text — do not ask
it for anything with words in the image.
aria.report.generate_image — OpenAI gpt-image-1. Real per-call cost, so
it requires confirmed: true; call aria.report.estimate_image_cost first.
Markedly better at instruction-following, flat/graphic styles, and legible
text.
Put the palette's hex values in the prompt. An image generated without them
will land near-but-not-on your colors, which reads worse than an obviously
different image.
Source stock instead when you need real people, real places, or
photojournalistic credibility — generated humans still fail on hands, teeth,
and crowd scenes, and a generated "customer photo" on a testimonial is a
misrepresentation, not a style choice. Point the user at Unsplash/Pexels
(free, permissive) and have them confirm the licence for commercial use
themselves.
Logos and brand marks
- Real company logos: Simple Icons only. Do not draw them —
scripts/fetch_icon.py --library simple-icons --name "<brand>" pulls the
real mark. Note Simple Icons slugs are NOT kebab-case like the other four
registries (americanexpress, nodedotjs, cplusplus, not hyphenated) —
the script handles this, a manual URL guess likely won't.
- The user's own logo: ask for the file. Do not generate a substitute and
present it as theirs.
- A new logo for a new brand is a real design engagement, not an asset-fetch
task — say that plainly rather than generating something disposable.
Placeholder content
Grey boxes and lorem ipsum hide layout problems until they are expensive to
fix. Use realistic content: plausible names, real-length copy, and images at
the aspect ratio the real ones will use. A card designed against a 3-word
title breaks the moment a 14-word title arrives.
Workflow
- Confirm the design direction exists (style, palette, type). If not, stop —
that is
industry-design-direction's job, and assets chosen before a
direction will not match it.
- Pick one icon set from the table, matched to the aesthetic, and state the
install/import method.
- Decide the imagery kind from
references/imagery_direction.md.
- For generated imagery: build prompts that carry the palette hex values,
estimate cost if using the paid backend, and generate.
- For stock or real logos: point at the correct source; do not fabricate.
- Run the quality gate — prefer
scripts/asset_lint.py over eyeballing the
three mechanically-checkable items (see Automated Checks below).
Automated Checks
Three of the Quality gate items below are mechanical, not judgment calls, so
scripts/asset_lint.py checks them by grepping the actual generated code
instead of relying on the gate being applied by eye:
python scripts/asset_lint.py path/to/src # scan real files/dirs
python scripts/asset_lint.py --demo # no data needed
emoji_icon (error) — an emoji character standing in for an interface icon.
invented_svg_path (warn) — a long inline <path d="..."> with no nearby
source citation (an import, a // source: ... comment). Can't prove the
path data was hand-recalled rather than fetched, but this is exactly the
shape that failure takes — a "logo" with no cited source renders as a blob.
mixed_icon_set (warn) — imports from more than one known icon library
(Lucide, Heroicons, Phosphor, Tabler, react-icons) across the scanned paths.
The remaining Quality gate items (accessible names, palette hex values in
generated-image prompts, no fabricated people/places/brands, realistic
placeholder length) stay judgment calls this script doesn't attempt —
automating those would launder a guess as a verified fact, which is worse
than leaving them as prose. Note this script is intentionally separate from
ui-design-system's design_lint.py: that one enforces code against the
user's own declared design-tokens.json, a different, tokens-shaped concern
from universal icon/asset hygiene, which applies whether or not a design
system has been frozen yet.
Quality gate
References
references/imagery_direction.md — which kind of imagery suits which design
direction, prompt patterns that keep generated images on-palette, and the
cases where generating is the wrong call
1---2name: ui-asset-sourcing3description: Source the actual assets a website or app needs — icons, imagery, logos, avatars, empty-state art — without hallucinating SVG paths, shipping emoji as icons, or dropping in stock photos that fight the palette. Trigger for "what icons should I use", "给这个网站配图", "add icons to this UI", "generate a hero image for this landing page", "这个空状态要放什么插图", "need a logo for this brand", or whenever a design direction exists and the remaining gap is the visual material to fill it. Names which icon set fits which aesthetic, how to fetch real icon and brand SVGs rather than inventing path data, and how to generate on-palette imagery with Aria Code's own image tools (`aria.report.generate_image_local` free/local, or `aria.report.generate_image` paid) when a stock photo would be wrong. Do NOT trigger to choose the design direction itself (use `industry-design-direction`) or to compile a single editorial poster (`minimal-editorial-poster`). Portable: self-contained prose, no script dependency.4---56# UI asset sourcing78A design direction with no assets becomes a wireframe with lorem ipsum and9grey boxes. Filling those boxes is where two specific, very visible failures10happen:11121. **Invented SVG path data.** A model asked for "a Stripe logo" or "a13 calendar icon" will confidently emit a `<path d="M12 2L2 7...">` that14 renders as an unrecognizable blob. Path data cannot be recalled correctly.152. **Emoji standing in for icons.** 🚀 in a feature card is the single fastest16 tell of generated UI, and it breaks cross-platform (rendering differs per17 OS) and for screen readers.1819Both are avoidable by *fetching* rather than *recalling*. That is most of what20this skill is.2122## Icons2324**Pick one set and stay in it.** Mixing sets is visible immediately — stroke25weights and corner radii will not match, and the interface reads as assembled26rather than designed.2728| Set | Fits | Notes |29| --- | --- | --- |30| Lucide | Most product UI; the safe default | ~1500 icons, consistent 24×24 / 2px stroke, MIT |31| Heroicons | Tailwind projects | Outline + solid pairs, by Tailwind's authors, MIT |32| Phosphor | When you need weight variation | 6 weights including duotone, flexible, MIT |33| Tabler | Dense/data UI needing breadth | ~5000 icons, 24×24 / 2px stroke, MIT |34| Material Symbols | Android/Material products | Variable axes (weight, fill, grade), Apache 2.0 |35| Simple Icons | **Brand/company logos only** | Official marks — the correct source when you need a real logo |3637**How to get the actual SVG** — never write path data from memory:3839- Ask the user to install the package (`lucide-react`, `@heroicons/react`,40 `@phosphor-icons/react`, …) and import icons by name. This is the right41 answer for almost every real project: the names are memorable and correct,42 the path data comes from the package.43- If a raw SVG file is genuinely needed and you have network access,44 `scripts/fetch_icon.py` pulls it from the registry's actual published45 source (Lucide/Heroicons/Tabler/Phosphor/Simple Icons — see Automated46 Fetching below) instead of hand-writing it — this is what closes the loop47 `asset_lint.py`'s `invented_svg_path` check can otherwise only catch after48 the fact.49- If you do not have network access, say so and give the import line50 instead — a stated limitation is better than a plausible-looking wrong path.5152## Automated Fetching5354```bash55python scripts/fetch_icon.py --library lucide --name arrow-right # prints real SVG to stdout56python scripts/fetch_icon.py --library lucide --name arrow-right --out a.svg57python scripts/fetch_icon.py --list-libraries58python scripts/fetch_icon.py --demo # offline, no network needed59```6061Fetches from each registry's real published URL (`unpkg.com`/`raw.githubusercontent.com`62for Lucide/Heroicons/Tabler/Phosphor/Simple Icons — see the script header for63the exact templates) and validates the response actually parses as SVG with a64real shape element before accepting it — several of these CDNs return HTTP 20065with an HTML or plain-text error body for a bad name, so status code alone66isn't proof. Fails loud with the resolved URL on a miss rather than writing67garbage to disk. `--demo` proves the fetch/validate logic offline by injecting68fixed responses (valid SVG, a disguised 404 page, an empty body) — it does not69require network access, but real usage does; `--list-libraries` shows every70registry's URL template without fetching anything.7172Deliberately does not vendor icon files into this repository — a copy here73would silently go stale the moment the upstream registry updates, the same74"quietly wrong forever" failure this catalog's other data-integrity gates75exist to catch. Fetch at generation time instead of storing a copy.7677**Consistency rules that matter more than the set you picked:**7879- One nominal size (usually 24×24) with a fixed stroke width across the whole80 UI; scale via the container, not by redrawing at another weight.81- Icons that carry meaning need an accessible name (`aria-label`, or adjacent82 text). Decorative icons get `aria-hidden="true"` so they are not announced.83- An icon alone is rarely enough for a primary action — pair with a label84 unless the metaphor is universally understood (close, search, back).85- Match the icon style to the aesthetic: outline icons for minimal/Swiss,86 filled/duotone for playful/claymorphic, sharp geometric for brutalist. A87 rounded friendly icon set inside a brutalist layout fights the direction.8889## Imagery9091Decide first **what kind** of imagery the direction calls for — see92`references/imagery_direction.md` for the per-category guidance (photography,93illustration, abstract/gradient, 3D, or deliberately none). The wrong *kind*94is a bigger error than a mediocre execution of the right kind.9596**When to generate rather than source stock:**9798Generate when the image must sit inside the palette you already chose, when99the subject is specific to this product, or when stock would read as100generic-corporate (the smiling-team-around-a-laptop problem). Aria Code has101two backends, both MCP-exposed:102103- `aria.report.generate_image_local` — self-hosted SDXL-Turbo. No API key, no104 per-call cost. Good for texture, abstract backgrounds, and light restyling.105 Weak at aggressive restyling and **cannot render legible text** — do not ask106 it for anything with words in the image.107- `aria.report.generate_image` — OpenAI `gpt-image-1`. Real per-call cost, so108 it requires `confirmed: true`; call `aria.report.estimate_image_cost` first.109 Markedly better at instruction-following, flat/graphic styles, and legible110 text.111112Put the palette's hex values in the prompt. An image generated without them113will land near-but-not-on your colors, which reads worse than an obviously114different image.115116**Source stock instead** when you need real people, real places, or117photojournalistic credibility — generated humans still fail on hands, teeth,118and crowd scenes, and a generated "customer photo" on a testimonial is a119misrepresentation, not a style choice. Point the user at Unsplash/Pexels120(free, permissive) and have them confirm the licence for commercial use121themselves.122123## Logos and brand marks124125- Real company logos: **Simple Icons** only. Do not draw them —126 `scripts/fetch_icon.py --library simple-icons --name "<brand>"` pulls the127 real mark. Note Simple Icons slugs are NOT kebab-case like the other four128 registries (`americanexpress`, `nodedotjs`, `cplusplus`, not hyphenated) —129 the script handles this, a manual URL guess likely won't.130- The user's own logo: ask for the file. Do not generate a substitute and131 present it as theirs.132- A new logo for a new brand is a real design engagement, not an asset-fetch133 task — say that plainly rather than generating something disposable.134135## Placeholder content136137Grey boxes and lorem ipsum hide layout problems until they are expensive to138fix. Use realistic content: plausible names, real-length copy, and images at139the aspect ratio the real ones will use. A card designed against a 3-word140title breaks the moment a 14-word title arrives.141142## Workflow1431441. Confirm the design direction exists (style, palette, type). If not, stop —145 that is `industry-design-direction`'s job, and assets chosen before a146 direction will not match it.1472. Pick one icon set from the table, matched to the aesthetic, and state the148 install/import method.1493. Decide the imagery *kind* from `references/imagery_direction.md`.1504. For generated imagery: build prompts that carry the palette hex values,151 estimate cost if using the paid backend, and generate.1525. For stock or real logos: point at the correct source; do not fabricate.1536. Run the quality gate — prefer `scripts/asset_lint.py` over eyeballing the154 three mechanically-checkable items (see Automated Checks below).155156## Automated Checks157158Three of the Quality gate items below are mechanical, not judgment calls, so159`scripts/asset_lint.py` checks them by grepping the actual generated code160instead of relying on the gate being applied by eye:161162```bash163python scripts/asset_lint.py path/to/src # scan real files/dirs164python scripts/asset_lint.py --demo # no data needed165```166167- `emoji_icon` (error) — an emoji character standing in for an interface icon.168- `invented_svg_path` (warn) — a long inline `<path d="...">` with no nearby169 source citation (an import, a `// source: ...` comment). Can't *prove* the170 path data was hand-recalled rather than fetched, but this is exactly the171 shape that failure takes — a "logo" with no cited source renders as a blob.172- `mixed_icon_set` (warn) — imports from more than one known icon library173 (Lucide, Heroicons, Phosphor, Tabler, react-icons) across the scanned paths.174175The remaining Quality gate items (accessible names, palette hex values in176generated-image prompts, no fabricated people/places/brands, realistic177placeholder length) stay judgment calls this script doesn't attempt —178automating those would launder a guess as a verified fact, which is worse179than leaving them as prose. Note this script is intentionally separate from180`ui-design-system`'s `design_lint.py`: that one enforces code against the181*user's own declared design-tokens.json*, a different, tokens-shaped concern182from universal icon/asset hygiene, which applies whether or not a design183system has been frozen yet.184185## Quality gate186187- [ ] Zero emoji used as interface icons?188- [ ] Zero hand-written SVG path data — every icon and logo either imported by189 name, fetched from a real source, or explicitly flagged as needing the190 user to fetch it?191- [ ] One icon set throughout, at one nominal size and stroke width?192- [ ] Meaningful icons have accessible names; decorative ones are193 `aria-hidden`?194- [ ] Generated imagery carries the actual palette hex values in its prompt?195- [ ] Nothing generated that misrepresents a real person, place, or company —196 no synthetic "customer photos", no drawn-from-memory brand marks?197- [ ] Placeholder text at realistic length, not lorem ipsum at convenient198 length?199200## References201202- `references/imagery_direction.md` — which kind of imagery suits which design203 direction, prompt patterns that keep generated images on-palette, and the204 cases where generating is the wrong call