Icons8 illustrations
A picking skill. It turns a brief into a set of illustrations that survives a
designer's review: right subject, one family, shippable files. It does not pick
icons (that's icons8) and does not design the page around them.
Operating posture
You are the designer choosing artwork for someone else's product. The bar: a person who knows that product looks at the page and says "yes, that's ours". Generic correctness is not the bar. An empty state that is technically an empty state but shows a browser window on an app about houseplants fails.
Three failure modes, worst first:
- The picture is about the interface, not about the product. This is the one that gets through review unnoticed and is the most common failure in testing. The slot is "empty plant list"; the answer is an empty pot, a bare shelf. It is not a plant (the heading says there are none), not a generic empty box, and definitely not an empty browser window.
- The file cannot be shipped. The permanent link is watermarked; the clean link dies in an hour. Get this wrong and the page is unpublishable, however good the picks.
- The set does not read as a set. Mixed shapes in one row, mixed drawing manner, the same picture used for two different states.
Never present a menu of options. Pick, say why in one line, move on.
Hard rules
- Never invent an id or a style
pretty_id. Both come from tool responses. A wrong style returns{"total": 0}with no error, which looks exactly like a subject that does not exist. - Every slot query carries the product's own noun.
empty+plant,offline+warehouse,invoice+payment. State alone is not a query. Except where the state is an absence — of content or of connectivity. "Nothing here yet", offline and 404 are settled conventions: an empty box, an empty shelf, a dead wifi fan, the 404 numerals. Showing the product present contradicts the words next to it, and reviewers read that as a mistake, not as domain flavour. Tested: "No bikes added" got a person standing with a bicycle, and the bicycle is right there in the frame. For absence, obey the convention; put the product noun in the object that is missing (an empty bike rack, an empty plant pot), never in a happy owner. Show the container, not the inhabitant. "Your team is empty" is an empty seat, an empty invite card, an outlined placeholder; it is not one person, and a lone figure is no better than a crowd. Two independent runs shipped a single user under that heading and a blind reviewer failed both. - Preview links never ship, and originals are never pulled for candidates.
preview.urlis watermarked, so it is for choosing only; the clean file fromget_illustration_png_urlorget_illustration_svggoes into the project. Paid works count against the account's download allowance (free-tier ones do not), so fetch originals once, for the approved set. One sanctioned exception: the single download the contrast gate needs (step 4) when the background is not white. - Shape is a layout problem, not a picking problem. One fixed box per row
plus
object-fit: contain. Never crop or stretch artwork to match a neighbour. - One illustration, one slot, and one motif. Reusing the same artwork for two states is a defect even when both states are errors. Different ids are not proof of different pictures: the catalog holds near-twins with identical headings, and a blind reviewer reads "Calendar with marked days" in two slots as one picture used twice. Compare headings, not just ids.
- Test the style against the real background before locking it, whenever the background is not white.
- Public or redistributable deliverable means
free_only: true, and the page says where the art came from.
When rules collide
Resolve in this order, higher wins:
- The file can be shipped: licence (
free_onlyfor public work) and format. - An existing page's style. Matching it beats everything below.
- Subject coverage: the style must draw your slots.
- Surface and contrast on the real background.
- Tone.
- The first tier.
A higher rule overriding a lower one is normal work, not a violation; name the override in the run notes. Tested: the bakery run failed because tone (5) was allowed to veto coverage (3).
The sequence
1. Name the surface
Everything downstream follows from this. Pick one row.
| Surface | Style family | Size on screen | Disqualifies |
|---|---|---|---|
| Product UI, empty and error states | one flat or line style, simple single objects | 120-180 px | busy scenes, crowds, anything needing detail to read |
| Docs, help centre, changelog | monochrome or two-tone line art | 64-140 px | colour-saturated art, 3D, anything that outshouts a code block |
| Marketing landing, feature blocks | 3D or rich flat | 280-560 px | thin line art that disappears at hero size |
| Slide deck | solid filled shapes, high contrast | 300-600 px | thin strokes, fine hatching, small internal detail |
| Open-source or public repo page | free-tier styles only | any | anything with free_distribution: false |
| Extending an existing page | the style already on the page, nothing else | match what's there | a "close enough" neighbour style |
Tested consequence: on a docs page, a colourful flat style reads as marketing even when every subject is right. On a dark deck, a style picked for its business tone put a dark grey phone on a near-black slide.
Then narrow by the voice the product speaks in. The surface says how loud; the tone says which family. Read the product's own copy before deciding: a warm second-person landing and an enterprise security page are not the same brief even when both are marketing.
Candidates before the semicolon are first tier (the curated shortlist in
references/STYLES.md); after it are fallbacks for when the tier does not cover
the slots. Leaving the tier is allowed, with the reason written into the run
notes.
The "avoid" column is a preference, not a filter. It ranks styles that all have your subject. If the only styles drawing your subject sit in the avoid column, they win: a warm bakery brief is better served by a 3D loaf than by a sketch of a picnic basket that is not bread.
| Tone | Take from | Avoid |
|---|---|---|
| Warm, human, informal | anthropic-claude-hand-drawn, rough-sketch, watercolor-sketch, ballpoint-pen, black-chalk, scribbles, open-doodles |
polished 3D, glassy, anything that reads rendered |
| Neutral product voice | concept, tint, organic, burst, company; outside: little, neat, plain, system |
painterly, ornate, heavy 3D |
| Corporate, enterprise | 3d-business, 3d-blueprint, marks, network; outside: 3d-enterprise, 3d-pro, isotech |
playful and kids styles |
| Premium, high-ticket | 3d-glassy, 3d-product, 3d-plastic, grain; outside: lucent, graphite |
doodle, crayon, sketch |
| Playful, consumer, kids | kindy, 3d-kindy, mochi, 3d-plush-icons, 3d-rondi; outside: 3d-playful, bouncy |
corporate 3D, line art |
| Developer-facing | notion-line-art, network, marks; outside: line, system, isotech |
character scenes, mascots |
Platform changes the complexity, not just the size. On mobile the picture lands at 120-180 px: one object, no crowds, no text inside the artwork, no scenes that need reading. On desktop a hero at 280-560 px can hold a scene with two or three actors. A deck at projector distance wants solid shapes and no internal detail at all. Same style can serve all three; the pick inside it cannot.
2. List the slots, each with its subject noun
Write the list before searching. Each line is slot + product noun:
empty plant list → empty pot, bare shelf (absence: container, no plant)
no search results → magnifier + plant
offline → wifi, disconnected (connectivity convention)
photo upload failed → plant photo, upload, error
The state-to-query translations live in references/VOCABULARY.md and the
slot kits per project type in references/SLOTS.md; open them before writing
the list.
Style choice depends on covering this whole list. A style that nails the hero and has nothing for the failure states is the wrong style.
2b. If the product has a specific noun, find out who draws it first
Before shortlisting styles in step 3, run one unfiltered search on the
product's noun and
read the styles field of the results. That single call tells you which styles
actually carry the subject, and the shortlist starts from the intersection of
that list with the tone.
search_illustrations(query="bread loaf bakery", amount=6) → 320 hits
→ styles seen: pablita, clip, burgundy, 3d-casual-life ×3
Skipping this is how a bakery app ends up with no bread in it. Tested failure: a
run took the tone table at its word ("warm and human, avoid polished 3D"),
checked fourteen styles that fit the tone, found bread in none of them, left the
tier for one more style without bread, and shipped a picnic basket, a trolley, a
bank card and a cardboard box. 3d-casual-life is first tier, holds several
loaves, and was never tried, because the tone rule had already excluded it.
Subject availability outranks the tone table. Tone decides between styles that have your subject; it does not get to veto the only styles that have it. When they conflict, take the subject and say so in the run notes.
3. Shortlist styles, then resolve the exact id
references/STYLES.md opens with the first tier: 43 styles the Icons8 side
picked out of the catalog (346 styles on 2026-09-07) as the ones worth reaching
for first. Build the shortlist from
there, take two or three, call list_illustrations_styles to get exact
pretty_id values, and run one search per slot against each candidate. Count
hits, then commit.
The tier is a preference, not a fence. Two situations override it outright, and both are correct:
- An existing page already has a style. Matching it beats the tier, always.
- The deliverable must be redistributable. The tier holds only two free
styles,
scribbles(99) andopen-doodles(37), too small for a page, so a public repo usually lands oncherryorfogg-5from outside.
Otherwise go outside when no first-tier style covers the slots or when the brief asks for a look none of them has. Either way name it in the run notes in those words: which style, and which rule sent you outside the tier. Silently ignoring the tier and silently obeying it are both wrong; the reason is the deliverable.
Coverage is per slot, not per style size: 3d-casual-life holds 2653
illustrations and only 30 of them answer empty.
4. Contrast gate, when the background is not white
Before locking, look at one candidate on the real background:
| Background | Reject |
|---|---|
| Dark (below ~#2a2a2a) | artwork whose main object is dark grey, black or navy; thin dark outlines; line art built from black strokes |
| Coloured or brand-tinted | artwork whose dominant hue fights the ground |
| Photo or gradient | artwork with an opaque light rectangle behind it |
Download one file, put it on the actual background, and look. A style name means nothing here: "Midnight" is line art for light pages.
5. Even out shape in the layout, not in the picking
Illustrations in one style come in every proportion, and coverage is often two to four candidates per slot, so filtering by ratio leaves you with nothing: four test runs tried, and every one ended with a mixed row anyway. Fix it where it is always fixable, in CSS:
.slot-art { height: 180px; display: grid; place-items: center; overflow: hidden; }
.slot-art img { max-height: 180px; max-width: 100%; width: auto; object-fit: contain; }
Three rules, each earned by a broken render. The measurements behind them are
in references/LAYOUT.md.
- Repeat the number, never
max-height: 100%. Percentage heights on SVG fail silently: a 269×563 file rendered 548 px tall inside a 180 px box and covered the heading under it. max-heightnever upscales. Served files run from ~60 px to 1000 px intrinsic with no relation to the artwork's importance. Artwork larger than the box getsmax-height: <box>px; artwork smaller than the box gets an explicitheight: <box>px(or a width). Then screenshot the page: both size failures in testing were caught only in a render, never by reading the CSS.- 3D artwork is not centred by its own box. Shadows and ground contact pull the mass up to 14% of the frame off centre, and in a row of cards that reads as "one of them slipped". Align the ground line, not the centre of mass: objects that stand (a pack, a box, a phone) line up by their bottoms. The centroid is only for subjects that genuinely float (a balloon, a character mid-air), because anything detached from the main object drags it.
Measure, do not eyeball:
python3 "${CLAUDE_PLUGIN_ROOT}/skills/ouch/scripts/measure.py" assets/illustrations/*
# per file: ground-line offset %, mass offset %, saturation
On Windows the interpreter and the variable are both spelled differently, and
${CLAUDE_PLUGIN_ROOT} expands in neither shell:
py -3 "$env:CLAUDE_PLUGIN_ROOT\skills\ouch\scripts\measure.py" assets\illustrations\* # PowerShell
py -3 "%CLAUDE_PLUGIN_ROOT%\skills\ouch\scripts\measure.py" assets\illustrations\* # cmd
The script expands * itself, so the pattern works in both.
The script ships with this skill; installed as a bare skill rather than a
plugin, it sits in this skill's own scripts/ folder. It needs Python 3 with
Pillow (python3 -m pip install pillow, or uv run the script) and Chrome or
Edge to rasterize SVG; PNG needs no browser. Do not re-derive the formulas
inline: two hand-copied versions have already drifted apart once, and the eval
gates import the same functions.
Then apply the numbers:
- Every set on the page counts as a row. Two phone screens side by side, four state mockups, three cards: if a reader sees them together, they are a row. A run nudged the cards and left the phone pair untouched because the brief only used the word "row" for the cards.
- In a set, normalise all of them, do not apply a threshold. Nudge each
picture by its own offset so all bottoms (or masses) land on one line:
−16.4% → translateY(+32.8px)at a 200 px box. The card left alone at 3.2% because it was "under the threshold" became the one a blind review called slipped. A threshold only makes sense for a single picture standing alone.
Flat and line styles rarely need this; every style with a drop shadow does. Two things still belong to picking: prefer candidates whose ratio is closer to the box when you have the choice, and never stretch or crop artwork to force a shape.
6. Look at the set before you commit
Build one contact sheet from preview.url (free, no calls) and open it:
<!-- sheet.html: each cell = <img src="PREVIEW_URL"> + heading + WxH -->
Check five things: every picture is about the product, no two pictures are the same, the drawing manner is identical across the set, every picture is the same kind of subject, and every picture uses the same colour treatment.
One style id is not one look. Styles hold both plain-ink and duotone
drawings: inside little, two picks measured 0.04 and 0.06 saturation next to
three at 0.50 to 0.59, and a blind reviewer called the set "split in half".
There is no recolour tool, so replace the odd ones at selection time. Measure
rather than eyeball: near-grey and low-saturation colour look the same on a
contact sheet and different on the page.
python3 "${CLAUDE_PLUGIN_ROOT}/skills/ouch/scripts/measure.py" <files> # same tool as step 5; saturation column
# Windows: the `py -3` lines in step 5 apply here too
A truly colourless work (under 0.10) next to a colourful one (over 0.35) is a broken set: replace the odd one. A run that had this rule in words still shipped such a set, so treat the number as the check, not the intention.
The number only catches the gross split. After it, look: same hue family, same accent, same amount of colour per picture. Four blue icons and one steel-grey one measure alike and still read as two sets.
Decide the register up front, all figures or all objects, and hold it. A full-length figure and a single object, both fitted to a 200 px box and both filling their frame, still read at different scales: the figure shrinks its own head to fit while the object keeps its bulk. Mixing them cannot be fixed in CSS. Same for jobs: an icon among characters, or a scene among single objects, gets replaced.
Selection happens entirely on previews, and previews are free: the link arrives inside the search response, needs no second call and costs no download. Pulling an original for a candidate you have not committed to is the one thing that wastes the account's allowance.
7. Download the clean files, write the lock
One call per approved slot, save into the project, point the HTML at local files.
| Need | Call | Result |
|---|---|---|
| Flat, line, hand-drawn art | get_illustration_svg |
vector, 1-70 KB measured |
| A thumbnail or a contact sheet | get_illustration_png_url(size="low") |
the small raster; needs no plan |
| Anything at card size | get_illustration_png_url(size="standard") |
456 px long side; needs no plan |
| A real hero | get_illustration_png_url(size="hd") |
largest source; size varies per artwork (2048-3000 px seen), read the returned width/height, resize before shipping. Needs an account whose plan covers the illustration, the same as SVG |
| 3D styles | PNG only | 3D artwork has no SVG at all |
Then write ouch.json next to the project so the next session stays on the
style:
{ "style": "notion-line-art", "target_ratio": 1.0,
"slots": { "hero": { "id": "6a3d01f2fae3aa473512807f",
"heading": "Hotline assistant answering customer inquiry",
"file": "assets/illustrations/hero.svg",
"width": 1000, "height": 1000,
"free_distribution": false } } }
Reject these
| Reject | Why | Seen in testing |
|---|---|---|
| A UI-generic picture where the product has a subject | reads as a stock screenshot of nothing | "Empty browser window" for an empty houseplant list |
| The same artwork in two slots | states stop being distinguishable | one warning triangle used for both "offline" and "upload failed" |
| An icon-shaped object among characters | breaks a set faster than a wrong subject | a line-art rocket dropped into a page of drawn people |
Brand logos, especially from 3d-fluency |
a logo is not an illustration for your block | most downloaded work of the year is a 3D WhatsApp logo |
| Lettering where a scene belongs | you get the word, not the picture | welcome returns the word WELCOME at 456×86 |
| Dark artwork on a dark ground | invisible from two metres | dark grey phone on a #12151a slide |
| A style with 20-60 works as a project base | runs out after a few slots | most of the free tier |
| Promising animation | animated: true filters, no tool returns an animated file |
a style marked animated still hands you a static webp |
Gotchas, all measured
A wrong
styleis silent, and zero results are not proof of anything.{"total": 0, "illustrations": []}comes back both for a mistyped id and for a subject this style does not cover. In testing an agent declared three live styles "gone" on this evidence alone. Before writing a style off, search it with one plain word (people,phone,box). Results mean the style is alive and your subject is the problem; zero on that too means the id is wrong, andlist_illustrations_stylessettles it.search_illustrationsreturnstotal. Atotalof 30 means the style barely covers that subject. Use it as the coverage number.Context is spent on searching, not on files. Measured order of magnitude:
Call Cost list_illustrations_categories,limit=30~8k tokens list_illustrations_styles,limit=100~3.4k search_illustrations,amount=10~1.6-1.9k search_illustrations,amount=3-5~0.5-0.9k get_illustration_svg/get_illustration_png_url~150-200 The file tools are cheap because they return a URL, not the artwork: a 1-70 KB SVG never enters the conversation unless you open it, and there is no reason to open it. Budget goes on categories and wide searches instead: narrow the category call, keep
amountat 5 for probing and 10 only for the slot you are actually filling.No documented pixel size is real; read the response.
standardreturned 456×456 against a documented 410×456;hdreturned 3000 px one day and 2048 px the next, depending on the artwork's source. The returnedwidth/heightis the only truth.Do not trust the size attributes inside the SVG file. On 2026-09-03 fresh downloads carried no
width/height(only aviewBox), collapsed to zero inside<img>and silently disappeared; on 2026-09-04 the same call returned files with both attributes. The server moves under you. The tool response always carries the numbers: put an explicit size on the<img>(step 5 says whenheightand whenmax-height) instead of relying on what is inside the file.Presigned URLs live one hour.
preview.urlis the only permanent link and it is watermarked. There is no link that is both.Two error texts, two causes. "needs an Icons8 sign-in" means the account has no active subscription, even when you signed in through OAuth. "No illustration , or no svg for it" means this artwork has no such format, which is normal for 3D and for some flat styles too. Neither means a bad id.
free_distribution: falseis not a licence warning. For a subscriber it is the ordinary case. But when the deliverable is public, filter withfree_only: trueand credit Icons8 on the page.
What to hand back
Per slot: the heading, the id, the style pretty_id, the local file path,
the downloaded file's intrinsic width×height, and free_distribution
copied from the server response, not guessed. Those last two are what someone else needs to check your licence and
your layout, and in testing they were the fields runs quietly dropped. Ids come
from search_illustrations only. If a format is refused,
say which and why rather than silently switching style. If the illustration
tools are unavailable, say so and stop.
Reference files
references/STYLES.md: styles by surface, the free tier in full, catalog facts.references/SLOTS.md: slot lists per project type, and thin subjects.references/VOCABULARY.md: slot to subject map, phrases that work.references/LAYOUT.md: the browser measurements behind steps 5 and 6.scripts/measure.py: ground line, mass offset and saturation for steps 5-6; the single source of those formulas. Python 3 + Pillow; Chrome or Edge for SVG.