Draws icons and app art in code - a Skia-path icon set, a brand mark, adaptive icons, splash, store art. Use when adding an icon, logo, app icon or splash, or an image or emoji appears. Not for photos (media-images-and-files).
Installs into .claude/skills of the current project.
Are you the author of Icons App Icon And Splash?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zakariaf-icons-app-icon-and-splash)
---
name: icons-app-icon-and-splash
description: Draws icons and app art in code - a Skia-path icon set, a brand mark, adaptive icons, splash, store art. Use when adding an icon, logo, app icon or splash, or an image or emoji appears. Not for photos (media-images-and-files).
---
# Icons, app icon and splash drawn in code
Makes every icon and every piece of brand art in an Expo app come from code: UI icons are layers on a
24-unit grid flattened into one Skia path each and shown by `<Icon>` as tinted native images; one brand
mark (layer data in theme colour roles) draws the in-app tile, the iOS light, dark and tinted icons, the
Android adaptive and themed layers, the splash and the Play icon. Two checkers prove the icon set, the
source bans, the rendered PNGs against each platform's rules, the config wiring and the mark.
## Rules that must hold
1. **Every UI icon is a named path in the app's icon set, drawn by `<Icon>`.** No icon font or
vector-icon package, no emoji, no image file or image import in app source (real photos and a
deliberately chosen package go into `contracts/app-art.json` allow-lists). react-native-svg is fine
for charts and illustrations; a UI icon drawn with it instead of `Icon` is a review finding (no
import scan can tell the two apart). Why: one rasterized path per icon laid out 4.8x faster than a
canvas per icon, the design owns every glyph, and emoji differ per OS and are read aloud by name.
(`no-icon-library`, `no-emoji`, `no-image-file`)
2. **`src/ui/icons/icon-paths.ts` is generated from `design/icon-layers.json` by
`scripts/art/build-icon-paths.ts`, never edited by hand.** It carries its `// GENERATED by` line,
holds exactly the layers' icons and directional list, and every path stays on the 24 grid and in
step with its layers; `--check` runs in the verify gate. Why: the layers are the design source, and
a hand-edited path drifts silently. (`icon-layers-missing`, `icon-paths-missing`,
`icon-layers-invalid`, `generated-header`, `icon-module-load`, `icon-set`, `icon-path-syntax`,
`icon-path-grid`, `icon-path-stale`)
3. **`Icon` is the only renderer.** It rasterizes once per name and pixel size, tints with a theme
colour, is hidden from VoiceOver and TalkBack (the control around it carries the label), and is the
only place raw `Image` and the right-to-left flip live. Which icons flip is rtl-and-direction's rule
(`directional-icons`). Why: colour changes never re-rasterize, and no call site can forget a label
or a flip.
4. **A glyph with two colours is two stacked icons** (`star-fill` under `star-edge`), each tinted from
the theme; three or more colours make it a picture. Why: an icon is a one-colour mask.
5. **One brand mark, as data, draws every picture of the brand.** `BRAND_MARK` in
`src/ui/icons/brand-mark.ts` lives on a 48 grid with 2 units clear at the edges (strokes included),
at most 8 layers, and theme colour roles (never hex); `BrandTile`, every app icon, the splash and
the store icon draw it. Why: a second drawing drifts on the first redesign, and roles make the mark
follow dark mode and the palette. (`brand-mark-missing`, `brand-mark-shape`)
6. **Multi-colour art is one Skia canvas built from shared, types-only draw code, never one per list
item.** `drawMark` takes the Skia API as a parameter, so the app, the CanvasKit goldens and the Node
scripts run the same code. Why: each canvas costs about 0.18 MB and 2.8 ms to mount, and a Skia
value import breaks the Node scripts.
7. **The app icon, splash and store icon come from `scripts/art/render-app-art.ts` and are committed.**
iOS: 1024 px, light opaque and full bleed, dark transparent, tinted opaque grayscale, the mark in the
central 62 %. Android: the foreground and the themed monochrome layer on transparency inside the
66 dp safe circle, the background layer opaque. Splash: the mark on transparency; Android 12+ shows
the image at `imageWidth` dp (at most 288) on a 288 dp icon masked to a 192 dp circle, so the mark
stays within 96 / `imageWidth` of the image from its centre (0.48 at the default 200). Play: 512 px, opaque, at most 1024 KB. `--check` runs
in the verify gate. Why: stores and launchers reject or crop art that breaks these, and a re-render
must not change bytes. Hand-made art follows the same platform rules (checked by eye).
(`art-missing`, `art-format`, `art-light-opaque`, `art-dark-clear`, `art-tinted-gray`,
`art-safe-area`, `art-android-foreground`, `art-android-background`, `art-android-monochrome`,
`art-splash-clear`, `art-splash-width`, `art-store-opaque`)
8. **The app config names every drawn image, and `expo-splash-screen` is installed before its plugin
entry exists.** Production iOS uses `iosAppearanceIcon()`, every variant's Android layers live in
`assets/icons/<variant>/` (app-variants-and-environment badges the testing copies), and the splash
is `splashPlugin()` on the palette's grounds. The checker reads the config as text;
`npx expo config --type public` per variant is the proof. Why: an unnamed file never ships, and a
plugin entry without its package stops `npx expo config`. (`art-unwired`)
9. **Look at every generated image before committing:** the icon contact sheet, and each app image at
1024 and 256 px. A new icon or mark is a design decision shown to the reviewer. Why: a valid PNG can
still be blank, clipped, filled solid or off centre.
10. **Repeated art in games and boards is pre-rendered once per key** (palette, scheme, colour-blind
flag, cell size, pixel ratio) into a sprite sheet, never redrawn per frame. Why: per-frame drawing
of identical art is the most common Skia jank.
## Workflow
1. Read [references/icon-system.md](references/icon-system.md): the decision, the layer model, the
build pipeline, `Icon`, tests. An app that cannot take Skia reads "If the app does not use Skia".
2. **Set up once.** Install `@shopify/react-native-skia` (2.6.2) and
`npx expo install expo-splash-screen` if missing. Copy from `templates/`, each to the path its first
line names (JSON files to their path under `templates/`): `design/icon-layers.json`,
`contracts/app-art.json` and its schema, `scripts/art/` (build-icon-paths, render-icon-sheet,
render-app-art, app-art-config, app-art-contract, art-cli, load-headless-skia and their tests) and
`src/ui/icons/` (icon-paths, icon-raster, icon, brand-mark-art, brand-mark, brand-tile, picture,
mark-paths and their tests). They import the canonical theme (`@/theme/*`), `useDirection`
(`@/i18n/direction-context`) and `@test/render-with-providers`, which design-system-structure,
rtl-and-direction and unit-and-component-tests create. Nothing to add to `jest.setup.ts`: its
central Skia mock already covers the rasterizer.
3. **Using an icon.** Pick the name in [references/icon-catalogue.md](references/icon-catalogue.md) (or
the app's own set), the size from the app's tokens, the colour from the theme; the surrounding
button or row carries the label. Slots: `icon={(look) => <Icon name="plus" {...look} />}`.
4. **Adding or changing an icon.** Follow icon-system.md, "Adding or changing an icon": layers into
`design/icon-layers.json`, `node scripts/art/build-icon-paths.ts`, then
`node scripts/art/render-icon-sheet.ts` and look at `reports/art/icon-sheet.png`.
5. **The brand mark and pictures.** Read
[references/brand-mark-and-pictures.md](references/brand-mark-and-pictures.md); replace the demo
layers of `brand-mark.ts` with the app's mark; draw empty states with `Picture` and layer data in
the feature folder.
6. **App icon, splash, store art.** Read [references/app-icon-and-splash.md](references/app-icon-and-splash.md)
and keep its order: dependency, `node scripts/art/render-app-art.ts`, look at all images at 1024
and 256 px, let app-variants-and-environment redraw the badged development and preview icons,
wire `iosAppearanceIcon()` and `splashPlugin()` into `app.config.ts`, then
`npx expo config --type public` per variant. Commit the images with the change that drew them.
Uploading store art is outward-facing: stop and ask the developer.
7. **Skia questions** (headless loading, Jest projects, text): [references/skia-notes.md](references/skia-notes.md).
8. **Run the checks** from the app root and loop until all pass: `npx tsc --noEmit` for both programs,
`npx eslint <changed paths> --max-warnings 0`, `npx jest src/ui/icons scripts/art --ci`,
`node scripts/art/build-icon-paths.ts --check`, `node scripts/art/render-app-art.ts --check`, then
the two checkers below. When multi-package, run each in every app folder. Once per app, put both
`--check` runs in the verify gate: add `"art:check": "node scripts/art/build-icon-paths.ts --check
&& node scripts/art/render-app-art.ts --check"` to `package.json` scripts and append `"art:check"`
to `verifyExtraSteps` in `quality-gates.json` (quality-gates' rule; a gated file, so the commit
carries the Gate-Change trailer: git-commits-and-reporting).
9. **Report** what was drawn or changed with the 256 px images and the contact sheet, and say plainly
that the reviewer should look at any new icon or mark.
## Definition of done
- [ ] `build-icon-paths.ts --check` and `render-app-art.ts --check` exit 0; both are in the app's
verify gate (quality-gates).
- [ ] The contact sheet and every app image were looked at (1024 and 256 px); new art was shown.
- [ ] No icon font, vector-icon package, emoji, image file or image import in app source beyond the
contract's allow-lists, and no UI icon drawn with react-native-svg.
- [ ] `npx expo config --type public` shows the drawn icons per variant and the splash on the palette
grounds; `expo-splash-screen` is a dependency.
- [ ] tsc, ESLint (`--max-warnings 0`) and the unit and golden Jest projects pass for the touched code.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-icons.mjs .` prints `RESULT: PASS` (before the app has
an icon set it prints `no icon set yet ...` and checks only the source bans)
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-app-art.mjs .` prints `RESULT: PASS` (expected only
while adopting: `SKIP src/theme/theme-types.ts [brand-mark-shape] not yet created
(design-system-structure creates it)`; `NOT APPLICABLE` until the renderer exists or
`contracts/app-art.json` declares rendered art: a contract holding only `icons` allow-lists
does not adopt the pipeline)
## Anti-patterns
- **Editing a path in `icon-paths.ts` to fix an icon.** The next generation overwrites it and the
checker flags it as stale; change the layers and regenerate.
- **An emoji or a PNG "just for now".** It ships; use an `Icon` or a `Picture`.
- **`@expo/vector-icons` because the template had it.** Two icon systems, glyphs the design does not
own; draw the icon into the set.
- **A UI icon as a react-native-svg `<Path>`** because the screen already draws a chart with it. It
bypasses the set, the tint, the RTL flip and the hidden-from-screen-readers default; use `Icon`.
- **A Skia canvas per row or tile.** Measured 4 to 5 times slower to lay out; icons in lists are `Icon`.
- **Flipping the mark, a play icon or a clock in RTL**, or flipping an icon at the call site. Only
icons that point along the reading direction flip, inside `Icon`.
- **Committing app art without looking at it,** or uploading store art without asking.
- **Importing Skia values in shared draw code.** The Node scripts then load React Native and fail;
import types and take the API as a parameter.
- **An app icon made from a screenshot, or a hex colour in the mark.** Render it from `BRAND_MARK` and
the palette so the icon, splash and in-app tile stay one design in both schemes.
- **A splash or Android foreground drawn edge to edge.** Android 12 and the launcher masks cut it to a
circle; keep the mark inside the safe circles.
- **Hand-made dark and tinted icons next to the generated light one.** They drift; render all three,
or keep an Icon Composer folder and list dark and tinted as optional.
## Files in this skill
| File | What it is | Read/run when |
|---|---|---|
| [references/icon-system.md](references/icon-system.md) | Why rasterized paths, the layer model, build pipeline, Icon, sizes, multi-colour glyphs, mirroring, tests, adding an icon, apps without Skia | Workflow step 1 |
| [references/icon-catalogue.md](references/icon-catalogue.md) | The 19 shipped icons: names, use, flips, layers | Picking or adding an icon |
| [references/brand-mark-and-pictures.md](references/brand-mark-and-pictures.md) | The mark's grid, layers and colour roles, designing a mark, BrandTile, Picture, shared draw code, canvas budget, sprites | Workflow step 5 |
| [references/app-icon-and-splash.md](references/app-icon-and-splash.md) | Outputs, the contract, iOS, Android, splash and store rules, the renderer, Expo config and variants, Icon Composer, review | Workflow step 6 |
| [references/skia-notes.md](references/skia-notes.md) | Where Skia runs, headless loading, APIs, declarative drawing, Jest projects, text | Any Skia question |
| `templates/design/icon-layers.json` | The icon source layers (19 neutral icons, the directional list) | Workflow steps 2 and 4 |
| `templates/contracts/` | app-art.json and its schema: where each image lives, optional images, glyph area, check-icons' allow-lists (an app that needs only the allow-lists keeps just the `icons` key) | Workflow steps 2 and 6 |
| `templates/scripts/art/` | build-icon-paths.ts, render-icon-sheet.ts, render-app-art.ts, app-art-config.ts (`iosAppearanceIcon`, `splashPlugin`), app-art-contract.ts, art-cli.ts (`--help`, bad-flag errors, exit codes), load-headless-skia.ts, and their tests | Workflow steps 2, 4 and 6 |
| `templates/src/ui/icons/` | icon-paths.ts (generated), icon-raster.ts, icon.tsx, brand-mark-art.ts, brand-mark.ts (demo mark), brand-tile.tsx, picture.tsx, mark-paths.tsx, with unit and golden tests | Workflow steps 2, 3 and 5 |
| `assets/shared.json` | The shared files this skill copies in (check-lib, app-modules, the canonical theme types for the fixtures) | When adding a shared file |
| `scripts/check-icons.mjs` | Checker: icon set vs layers (names, directional list, syntax, grid, coverage), generated header, and the icon-library, emoji and image bans in app source | Workflow step 8 |
| `scripts/check-app-art.mjs` | Checker: the rendered PNGs per platform rule, the splash width, the config wiring and dependency, the brand mark | Workflow step 8 |
| `scripts/lib/` | art-contract.mjs (reads `contracts/app-art.json`), svg-path.mjs (SVG path reader), icon-coverage.mjs (a path vs its layers, without Skia), app-modules.mjs (synced: imports the app's plain TypeScript) | Never directly |
| `scripts/check-lib.mjs` | Shared script helper, synced from the library (do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Proves both checkers on the templates and planted bugs | After changing a checker |
| `tests/` | `bases/app/` (the fixture app) and `fixtures/` (good, pass, bad and error cases; images generated at run time) | When adding a rule |
## Related skills
- `design-system-structure` - the palette and colour roles the icons and the mark use.
- `ui-component-library` - the controls that take icons through slots.
- `rtl-and-direction` - which icons flip (`directional-icons`).
- `app-variants-and-environment` - per-variant icon folders and the badged testing icons.
- `skia-canvas-rendering` - canvases, boards, text on canvases.
- `accessibility-as-code` - labels on the controls around icons; decorative canvases.
- `unit-and-component-tests` - the central Skia mock; `golden-and-snapshot-tests` - the golden project.
- `performance-budgets` - how many canvases a screen may hold.
- `media-images-and-files` - real photos and user images.
- `eas-build-and-submit`, `ios-release-testflight`, `android-release-play` - the builds and store listings that carry the art.