Codex vs Claude

Constructing SVG Icons in Code: viewBox, currentColor and the Rest Is Detail

David Guzenburg/ / 10 min read

An icon that looks right in the pull request and hardcodes its fill will be invisible in dark mode, and nobody will connect the two events.

toolingdesignconventionsquality gates

Two attributes decide whether an icon survives your design system

Most reviews of agent-written SVG focus on whether the shape looks right. That is the least durable property in the file. An icon that looks correct in the pull request and hardcodes fill="#000000" will be invisible in dark mode a month later, and nobody will connect the two events.

The attributes that decide an icon's fate are viewBox and currentColor. Everything else — path elegance, node count, whether the curve is a true arc — is refinement. These two are structural, and an agent gets them wrong in a specific, predictable way.

viewBox is the contract; width and height are a suggestion

A viewBox declares the coordinate system the paths are drawn in. It is what makes the graphic scalable: the browser maps that box onto whatever space the element occupies. Fixed width and height attributes on the root <svg> override that flexibility with a specific pixel size, which is exactly what you did not want.

<!-- Locked to 24px. Set font-size on the parent and nothing happens. -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="#111827">

<!-- Scales with the box it is placed in; inherits the text colour. -->
<svg viewBox="0 0 24 24" width="1em" height="1em"
     fill="none" stroke="currentColor" stroke-width="1.75">

The second form is the one that behaves. 1em ties the icon to the surrounding type scale, so an icon beside a heading grows with the heading without a single line of component code. If you take one habit from this article, take that one.

currentColor is how an icon joins the cascade

currentColor resolves to the computed color of the element, which means the icon inherits from whatever context it lands in: theme, hover state, disabled state, an alert's error colour. A hardcoded hex value opts out of all of it and forces every consumer to override.

Agents hardcode colours because the drawing tools and reference material they have learned from hardcode colours. Editor exports are full of #000000. It is not a reasoning failure so much as a convention that has to be stated, and stating conventions the model cannot infer is precisely what your repository's instruction file exists for — see encoding architecture constraints for the general shape of that argument.

Pick a family and stay in it

An icon set is either stroke-based or fill-based. Mixing them is the most visible inconsistency in a UI and the easiest to introduce one icon at a time. Write which one you use into the instruction file, with the stroke width, and the agent will hold the line better than a human reviewer does.

Stroke width does not scale the way you expect

Scale a stroked icon down and the stroke scales with it, so a 1.75px stroke at 24px becomes a 1.17px stroke at 16px — noticeably lighter beside its neighbours. The fix is vector-effect="non-scaling-stroke", which keeps the rendered stroke constant regardless of the transform.

Use it deliberately rather than everywhere. A set designed at one size and displayed at one size does not need it. A set that appears at 16, 24 and 40 does, or you accept that the small sizes read lighter.

Accessibility is two attributes and a decision

The decision is whether the icon carries meaning. A chevron beside the word "Next" carries none — the word does. A bare trash-can button carries all of it.

<!-- Decorative: the adjacent text already says it. -->
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">

<!-- Meaningful: this is the only label the control has. -->
<svg viewBox="0 0 24 24" role="img" aria-label="Delete item">

Agents default to labelling everything, which produces a screen reader that announces "chevron right" after every link. Over-labelling is a real accessibility defect, not a harmless excess, and it is worth saying so in the instructions.

What agent-written SVG gets wrong, in order of frequency

Hardcoded colours, as above. Then nested <g transform="translate(...)"> wrappers that exist because the paths were drawn in the wrong coordinate space and shifted afterwards — harmless to render, miserable to edit, and a sign the geometry was not reasoned about. Then editor cruft: id attributes referencing nothing, empty <defs>, xmlns:xlink declarations with no xlink in the file. Then paths with fifteen decimal places, which triple the file size and say nothing.

A validator beats a reviewer

All of the above is mechanically checkable, which means it should be checked mechanically. This runs in well under a second across a full icon set.

import re, sys, pathlib

FAILS = []

def check(p):
    s = p.read_text(encoding="utf-8")
    if "viewBox" not in s:
        FAILS.append((p, "no viewBox"))
    if re.search(r'(fill|stroke)="#[0-9a-fA-F]{3,8}"', s):
        FAILS.append((p, "hardcoded colour; use currentColor"))
    if re.search(r'<svg[^>]*\swidth="\d+"', s):
        FAILS.append((p, "pixel width on root svg; use 1em"))
    if "aria-label" not in s and 'aria-hidden="true"' not in s:
        FAILS.append((p, "neither labelled nor explicitly decorative"))
    if re.search(r"\d\.\d{5,}", s):
        FAILS.append((p, "excessive path precision"))

for p in sorted(pathlib.Path("assets/icons").glob("*.svg")):
    check(p)

for p, why in FAILS:
    print("%s: %s" % (p, why))
sys.exit(1 if FAILS else 0)

Five rules, each of which encodes a decision your team has already made. Add one every time a review comment repeats itself. The value is not that the script is clever; it is that the review conversation stops happening.

Inline, sprite or file: pick by how the icon is used

The same icon can be delivered three ways, and agents pick whichever appeared in the last file they read rather than the one that suits the situation.

Inline in the component is right when the icon needs to react — animate a path, change stroke on hover, swap a shape by state. You pay for it in markup size, repeated per instance. An external file referenced by <img> is right for anything decorative and static, and it gets cached, but it cannot inherit currentColor at all, which undoes half of the previous section. A sprite sheet with <use> is the middle path: one request, colour inheritance intact, one definition per icon.

The rule worth writing down

If the icon changes with state, inline it. If it must follow the theme, use a sprite. If it is decorative, static and large, use a file. State the rule in the instruction file and the choice stops being arbitrary.

Optimisation is a lossy operation on the parts you care about

Running an optimiser over agent-written SVG is nearly always worth it, and it is not free. Aggressive settings merge paths, drop id attributes something else was referencing, collapse groups that a transition depended on, and round coordinates until a hairline gap opens between two shapes that were flush.

Keep a conservative configuration in the repository rather than accepting tool defaults, and re-render the contact sheet after the optimiser runs rather than assuming a byte reduction was harmless. The specifics of which passes to disable, and what each one breaks, get their own treatment in the article on optimising vector assets.

Reviewing an icon in ninety seconds

Open the file. Is there a viewBox, and is it square and sensible — 0 0 24 24 rather than 0.5 1.25 23.1 22.8, which means the geometry was drawn somewhere else and nudged? Is the colour currentColor or absent? Is the root sized in em rather than pixels?

Then: one stroke width, matching the set. No nested transform wrappers. No id attributes unless a <use> needs them. Either aria-hidden or aria-label, matching whether the icon carries meaning. Coordinates at two decimal places, not fifteen.

Finally, look at it in the contact sheet beside its neighbours, in both themes. Every item on that list except the last is enforced by the linter above, which is precisely why the last one is the only part a person should be spending time on.

Optical correction is the thing an agent cannot compute

Mathematical centring and optical centring are different, and icons are judged optically. A triangle centred by its bounding box looks left-heavy, because its visual mass sits toward the base — which is why every play button you have ever seen is nudged a pixel or two to the right. A circle needs to be slightly larger than a square to read as the same size beside it.

An agent placing shapes by coordinate will centre them correctly and produce a set where the play icon looks off. This is not a prompting failure; it is a perceptual judgement made from a rendering, and the agent is not looking at a rendering. The contact sheet from the previous section is how you catch it, and the correction is a manual nudge you then record in the file so nobody undoes it.

<svg viewBox="0 0 24 24" width="1em" height="1em"
     fill="currentColor" aria-hidden="true">
  <!-- Optically centred: shifted +0.75 on x. Mathematical centre
       reads left-heavy against the square-bounded icons. -->
  <path d="M8.75 5.5v13l10-6.5z"/>
</svg>

The comment is doing real work. Without it, the next person to touch the file — agent or human — sees an asymmetric shape in a symmetric frame, assumes an error, and centres it.

Baseline alignment, the other invisible defect

An icon sized in em and dropped inline beside text sits on the text baseline, which puts it slightly too low, because a glyph's visual centre is above its baseline. The usual correction is a small negative vertical alignment, or setting the icon and its label in a flex row with align-items: center and letting the box model handle it.

Whichever you choose, choose once and put it in the icon component rather than in the twelve places icons appear. Agents will faithfully reproduce whatever the surrounding code does, so a codebase with three different alignment hacks will grow a fourth.

What to write in the instruction file

Everything in this article that is a decision rather than a fact belongs in your repository's agent instructions, stated once: the stroke width, whether the set is stroke or fill, the em sizing rule, the inline-versus-sprite-versus-file rule, the labelling convention, and the precision limit. Six lines.

Those six lines are worth more than any amount of review commentary, because they are read before the work happens rather than after. The linter then catches the cases where they were read and not followed, which is a much smaller set than the cases where they were never stated.

Why this list is short on aesthetics

Nothing here tells you whether an icon is good. That is deliberate, and it is the trade I would defend if you pushed back on this article: every rule above is mechanical because mechanical rules are the ones that survive delegation.

An agent can be told to use currentColor and will comply indefinitely. It cannot be told to make an icon feel like the rest of the set, because that judgement is made by looking, and the looking has to be done by someone with the set in front of them. Loading the review with both kinds of feedback means the mechanical half crowds out the perceptual half, and the perceptual half is the only one a person was needed for.

So: automate the structural rules completely, and spend the recovered attention on the contact sheet. That is the whole argument. The linter is not there because the rules are important — they are minor — it is there because the rules are boring enough to displace the important conversation if you let them.

Takeaway

Judge agent-written SVG on viewBox and currentColor before you judge the shape: those two decide whether the icon scales with its type and follows your theme, and both are wrong by default because editor exports taught the model otherwise. Set width="1em", pick stroke or fill for the whole set and write it down, label meaningful icons and hide decorative ones, and enforce all of it with a twenty-line linter rather than repeating yourself in review.

Keep reading
Codex vs Claude

Generated Placeholder Assets: Borrowing Against a Design You Have Not Made

Agent-generated placeholder icons unblock a build and quietly ship to production. Path conventions no production file may import, a four-line CI gate, watermarks, and the provenance record that makes the licensing question answerable.

Codex vs Claude

Mockup to Code: The Component Matches the Picture, Which Is the Problem

An image carries layout and proportion and omits breakpoints, states, content variance and semantics. Why generated UI is full of bracketed pixel values, and the token-extraction step that fixes it.

Codex vs Claude

The Favicon Set Is Four Files: Cutting the Package an Agent Gives You

Generated favicon packages encode a decade of obsolete device workarounds. The set that is actually requested, an SVG icon that follows the colour scheme, the iOS transparency trap, and a CI check for paths that quietly return HTML.

Codex vs Claude

One Agent, Many Surfaces: They Share a Model and Nothing Else

Terminal, editor, desktop, browser and hosted runner are different tools sharing an account. What differs, why consistency has to come from the repository, verifying permission denials per surface, and choosing a surface by the shape of the task.

← Optimising SVG: The Byte Count Reports the Saving, Not the Damage  ·  Fewer Tool Calls: Efficiency or Missing Evidence? →

All codex vs claude articles  ·  Every article