Skip to main contentSkip to docs navigation

Image Assets

Where images go in the source, how resolution variants are named, and what the build writes for web, iOS and Android.

Introduction

Images are the files under images/ of an app: logos, illustrations, photos, screenshots and backgrounds. The build copies them to every platform with the name the platform expects, and keeps the subfolders of images/.

An image has the variants the designer exported. The build does not resize a file, and by default it does not re-encode one. It reads the @2x or @3x indicator of a name, and on Android it sorts the variants into density folders. With the --asset-catalog option it sorts the variants of iOS into the image sets of an asset catalog.

Source layout

Images live in source/<brand>/<app>/images/, in any subfolders. The default brand holds the images of every brand, and a brand holds only the files that differ.

TEXT
source/default/demo/images/
├── chassis-logo.svg
├── chassis-logo-shadow.png
├── chassis-logo-shadow@2x.png
├── home/
│   ├── comp-gallery-dark.png
│   ├── comp-gallery-dark@2x.png
│   └── ...
└── logo/
    └── ...

A brand file replaces the default file of the same path, subfolder included. The sinefil brand of this repository has a logo/ folder only, so it gets its own logos and every other image of the default brand.

Formats

The build copies an image of any format. One format is the exception: iOS does not get WebP files.

FormatWebiOSAndroid
PNG, JPG, GIF, SVGCopiedCopiedCopied
WebPCopiedDroppedCopied

Export a WebP image in a second format, PNG or JPG, when an iOS app needs it too. The web and the Android output then hold both files. A page chooses between them with a <picture> element, and an Android app copies the one it uses to res/.

The designer does not have to export the WebP: a build with the --webp option writes one from each PNG and JPG, beside the file on the web and in place of it on Android, and --avif writes an AVIF for the web. A WebP of source/ is kept over the one the build would write. A build with --optimize writes a PNG or an SVG again under its name when that makes it smaller, without a change to the image. The build system page has the options and their settings.

PNG, JPG and WebP files are Git LFS files. SVG files are text and are committed as they are.

Naming

Name an image in lowercase with hyphens, after what it shows: comp-gallery-dark.png, chassis-logo-white.svg. Each platform renames the file, so the source name is written once and the web, iOS and Android names follow from it.

Variants

A variant is the same image exported at another resolution. Its name ends with an indicator before the extension: @2x for twice the size, @3x for three times. The file without an indicator is the base size.

TEXT
source/default/demo/images/logo/
├── chassis-icon-black.png
├── chassis-icon-black@2x.png
└── chassis-icon-black@3x.png

The web and the iOS output keep the indicator in the name. Android has no indicator: the build writes each variant under the same name into the density folder of its resolution, see Platform output.

Color modes

An image that differs between the light and the dark mode is two files, with the mode at the end of the name: comp-gallery-light.png and comp-gallery-dark.png. The build treats them as two images, and the app chooses between them.

Adding files

An image enters the repository as an export from the design tool, saved under images/ of the brand and the app it belongs to.

  1. Name the layer or the frame in the design tool as the file is to be named, without the extension.
  2. Add one export setting per variant: the base size, then 2x and 3x for a raster image that an app shows. Figma adds the @2x and @3x indicators to the names by itself. An SVG needs no variant.
  3. Export to source/default/<app>/images/ for an image of every brand, or to source/<brand>/<app>/images/ for an image of one brand.
  4. Run pnpm assets and look for the file in dist/.

A subfolder groups images that belong together, such as logo/ or home/. The build keeps the subfolder on every platform.

Best practices

The build copies an image as the designer exported it. These practices cover what to export and where to put it, so each platform gets the file it reads.

  • Export every variant an app needs: the build copies @2x and @3x files and does not make them. An image without variants reaches Android in drawable/ only.
  • Keep the base name the same across variants: hero.png, hero@2x.png and hero@3x.png become one Android drawable in three densities. A variant with another base name becomes another image.
  • Add a PNG or JPG beside a WebP: iOS does not get WebP files.
  • Override by path: a brand image replaces the default image only when the subfolder and the name are the same, images/logo/chassis-logo.svg for images/logo/chassis-logo.svg.

Avoid the habits that undo this:

  • Do not put icons of the interface under images/: the files of icons/ come from Chassis Icons, and Android gives them the ic_ prefix. See icons.

Platform output

The build copies each image to the images/ folder of the job, in its subfolder, and renames it for the platform.

PlatformFolderNameFormats
Webdist/web/<app>/<brand>/images/kebab-case, indicator keptEvery format
iOSdist/ios/<app>/<brand>/images/snake_case, indicator keptEvery format but WebP
Androiddist/android/<app>/<brand>/images/snake_case, in density foldersEvery format

The source file images/home/comp-gallery-dark@2x.png is images/home/comp-gallery-dark@2x.png in the web output and images/home/comp_gallery_dark@2x.png in the iOS output. In the Android output it is images/home/drawable-xhdpi/comp_gallery_dark.png:

TEXT
dist/android/demo/chassis/images/
├── drawable/
│   ├── chassis_logo.svg
│   ├── chassis_logo_shadow.png
│   └── ...
├── drawable-xhdpi/
│   └── chassis_logo_shadow.png
├── home/
│   ├── drawable/
│   │   ├── comp_gallery_dark.png
│   │   └── ...
│   └── drawable-xhdpi/
│       ├── comp_gallery_dark.png
│       └── ...
└── logo/
    ├── drawable/
    ├── drawable-xhdpi/
    └── drawable-xxhdpi/

On Android the indicator of a name chooses the density folder, and a file without an indicator goes to drawable/.

IndicatorDensity folder
nonedrawable/
@1xdrawable-mdpi/
@1.5xdrawable-hdpi/
@2xdrawable-xhdpi/
@3xdrawable-xxhdpi/
@4xdrawable-xxxhdpi/

The density folders are written inside each subfolder of images/, so images/logo/ has density folders of its own. The res/ folder of an Android app does not nest, so an app copies the density folders of each subfolder it uses into res/. A build with the --res option does that copy: it moves the raster images of every subfolder into res/drawable/ and the density folders, see the build system page.

Asset catalog

A build with pnpm assets --asset-catalog writes the images of an iOS job as an asset catalog, Assets.xcassets in place of images/. The files of one base name are one image set, each variant in the slot of its indicator, and a subfolder is a folder of the catalog:

TEXT
dist/ios/demo/chassis/
├── Assets.xcassets/
│   ├── Contents.json
│   ├── chassis_logo.imageset/
│   │   ├── Contents.json
│   │   └── chassis_logo.svg
│   ├── chassis_logo_shadow.imageset/
│   │   ├── Contents.json
│   │   ├── chassis_logo_shadow.png
│   │   └── chassis_logo_shadow@2x.png
│   ├── home/
│   └── logo/
│       ├── Contents.json
│       ├── chassis_icon_brand.imageset/
│       │   ├── Contents.json
│       │   ├── chassis_icon_brand.png
│       │   ├── chassis_icon_brand@2x.png
│       │   └── chassis_icon_brand@3x.png
│       └── ...
└── images/
    └── logo/
        ├── chassis_icon_brand.svg
        └── ...

An image set holds the raster variants of a name, or one SVG or PDF where the name has no raster. A file without a place in an image set stays in images/: here the SVG logos, which have PNG variants of the same name. The build system page has the rules of the option.

See the web, iOS, and Android docs for how an app uses these files.