Theming & Visual Design

Copy Markdown View Source

This guide explains how to colour and decorate a wallet pass with wallet_passes. Apple and Google diverge sharply: Apple bakes images into the signed .pkpass bundle, while Google references images by URL. The library offers a single Theme struct for the overlap (colours plus logo text) and per-platform Visual structs for the rest.

Overview

A pass has two layers of styling:

  1. Shared style — colours and a small piece of header text that appear on both platforms. WalletPasses.Theme carries this; Theme.to_apple_visual/1 / Theme.to_google_visual/1 convert it into the per-platform structs.
  2. Platform-specific assets — image files for Apple (icon.png, logo.png, strip.png, thumbnail.png, plus retina variants) and hosted image URIs for Google (logo_uri, hero_image_uri, image_modules). These live on WalletPasses.Apple.Visual and WalletPasses.Google.Visual and never overlap.

Theme is purely a convenience — skip it and build the two Visual structs by hand if that suits you better. The rest of the library never reads Theme directly.

Concepts

Apple: signed image set + hex colours

An Apple .pkpass is a ZIP file containing image files alongside pass.json. iOS reads those files locally — no network required after the pass is on-device. Colours are written into pass.json as CSS-style rgb(r, g, b) strings; the library accepts standard #RRGGBB hex on the Visual struct and converts on the way out.

{
  "backgroundColor": "rgb(26, 26, 26)",
  "foregroundColor": "rgb(255, 255, 255)",
  "labelColor": "rgb(212, 168, 67)"
}

The image set is fixed: Apple recognises specific filenames (icon.png, logo.png, strip.png, thumbnail.png, background.png, footer.png) and ignores anything else. Retina variants use the @2x / @3x suffix. Every file in the ZIP is hashed into manifest.json, which is PKCS#7-signed.

Google: hosted URIs + hex string for background

Google Wallet stores pass content server-side. Images aren't packaged — Google fetches them from URLs you supply and re-hosts them on Google's CDN. You're responsible for keeping those URLs reachable while the pass is in use.

{
  "hexBackgroundColor": "#1A1A1A",
  "logo": {
    "sourceUri": {"uri": "https://example.com/logo.png"},
    "contentDescription": {
      "defaultValue": {"language": "en-US", "value": "Logo"}
    }
  }
}

Google's hexBackgroundColor accepts the hex string directly — no conversion needed. There's only one background colour on Google (no foreground / label distinction), so most theme entries don't have a Google counterpart.

The Theme struct and conversion helpers

WalletPasses.Theme is a tiny shared-colour carrier:

defstruct [:name, :background_color, :foreground_color, :label_color, :logo_text]

:name is informational only (never written to either platform — use it to label the theme in your own code). The three colour fields are #RRGGBB hex strings. :logo_text is the small string drawn next to the logo on Apple passes; Google has no equivalent and ignores it.

Conversion helpers

theme = %WalletPasses.Theme{
  background_color: "#1A1A1A",
  foreground_color: "#FFFFFF",
  label_color: "#D4A843",
  logo_text: "My Event"
}

apple_visual =
  theme
  |> WalletPasses.Theme.to_apple_visual()
  |> struct!(
    icon_path: "priv/static/passes/icon.png",
    strip_image_path: "priv/static/passes/strip.png"
  )

google_visual =
  theme
  |> WalletPasses.Theme.to_google_visual()
  |> struct!(
    logo_uri: "https://cdn.example.com/passes/logo.png",
    hero_image_uri: "https://cdn.example.com/passes/hero.png"
  )

to_apple_visual/1 copies the three colour fields plus :logo_text into a fresh Apple.Visual; image paths come up nil. to_google_visual/1 copies only :background_color. Merge platform-specific assets in with struct!/2.

Hex → Apple rgb() conversion

The library converts #RRGGBB to Apple's rgb(r, g, b) form automatically at build time. The helper is public if you need it elsewhere:

iex> WalletPasses.Theme.hex_to_apple_rgb("#1A1A1A")
"rgb(26, 26, 26)"

iex> WalletPasses.Theme.hex_to_apple_rgb("#D4A843")
"rgb(212, 168, 67)"

Only #RRGGBB is supported. Short-form hex (#FFF), #RRGGBBAA, and named colours raise — keep stored values in #RRGGBB form. There is no inverse helper: Google takes the hex string straight from Visual.background_color and writes it verbatim.

Apple image set

WalletPasses.Apple.Visual carries paths on disk for three image slots:

FieldFilename in .pkpassRequired by Apple?
:icon_pathicon.pngYes
:strip_image_pathstrip.pngPass-type specific
:thumbnail_paththumbnail.pngOptional

The builder reads each path with File.read/1. Missing files are silently skipped — the pass still builds, but Apple will reject it on the device if icon.png is absent. Always provide at least icon_path.

Pass-type image conventions

Apple uses different "primary" images per pass style:

  • Event tickets — typically strip.png (full-width banner).
  • Boarding passeslogo.png plus footer.png; no strip.
  • Store cards / loyaltystrip.png or background.png.
  • Couponsstrip.png.
  • Genericthumbnail.png (right-aligned square) plus logo.png.

Apple.Visual exposes the three most common slots directly. For logo.png, background.png, or footer.png, ship them through the :localized_images option on Apple.Builder.build_pkpass/4 (it works for the default locale too — just key by your default-locale tag).

Retina variants and dimensions

iOS picks the highest-resolution match for the device. Provide retina variants by shipping icon@2x.png and icon@3x.png alongside icon.png — same for strip, thumbnail, etc. Apple.Visual doesn't expose dedicated fields for retina variants; ship them via :localized_images (filename keys like "icon@2x.png") or extend the builder.

The library does not validate dimensions or resize — whatever you provide ships verbatim, and iOS scales to fit. Exact pixel dimensions per pass type and image slot vary across iOS versions; see Apple's PassKit Package Format Reference for the canonical numbers. Per-locale image variants live in <locale>.lproj/ — see Localization.

Google image fields

WalletPasses.Google.Visual carries URIs (not paths) for the images Google's servers fetch:

FieldGoogle JSON pathNotes
:logo_urilogo.sourceUri.uriSquare logo, top-left of pass face
:hero_image_uriheroImage.sourceUri.uriWide banner across the top
:wide_logo_uri(struct-only, reserved)Not currently emitted into JSON
:image_modulesimageModulesData[].mainImage.*List of {uri, description} tuples

:wide_logo_uri is declared on the struct for forward-compatibility but is not written into the Google JSON by the current API builder. Set :logo_uri for the standard logo slot.

Content descriptions

Every image Google receives needs a contentDescription for screen readers. The library generates these automatically — "Logo" for the logo, "Hero image" for the hero, and the per-module description string you supply in :image_modules. Content descriptions can be localised; see the Localization guide.

Image hosting and modules

Google fetches every image once per object create/update and re-hosts it on Google's CDN. Practical constraints: HTTPS-only, reachable from Google's IP ranges, PNG/JPEG/WebP (no SVG). Pixel dimensions live in Google's image style guidelines; logos render in a small square, hero images render full-width across the pass top.

For longer passes that need additional inline imagery (event poster, venue map, sponsor logo), use :image_modules:

google_visual = %WalletPasses.Google.Visual{
  logo_uri: "https://cdn.example.com/logo.png",
  hero_image_uri: "https://cdn.example.com/hero.png",
  image_modules: [
    {"https://cdn.example.com/poster.png", "Concert poster"},
    {"https://cdn.example.com/map.png", "Venue map"}
  ]
}

Each tuple is {uri, content_description}; order is preserved into imageModulesData[]. Image modules are Google-only.

When to share colours vs use platform-specific visuals directly

Use Theme when the two platforms should share a palette — same background at minimum, ideally the same brand identity. A single %Theme{name: "Summer 2026"} constant declared once and consumed in both PassDataProvider builds is the canonical case.

Build Apple.Visual and Google.Visual directly when the platforms need genuinely different palettes (Apple's pass face has more visual chrome — foreground / label colours matter — while Google's flatter layout often looks better with different contrast), when lifecycle decoration (see Pass Lifecycle) needs per-platform hexes, or when you're testing one platform in isolation.

The two approaches are interchangeable; nothing else in the library requires Theme.

API Reference

WalletPasses.Theme %Theme{name, background_color, foreground_color, label_color, logo_text}. to_apple_visual/1 copies all four colour/text fields into an Apple.Visual (image paths nil). to_google_visual/1 copies only :background_color (URIs nil). hex_to_apple_rgb/1 converts "#RRGGBB" to "rgb(r, g, b)" and raises on malformed input.

WalletPasses.Apple.Visual :background_color, :foreground_color, :label_color (all #RRGGBB, converted to rgb() at build time), :logo_text, and the image paths :icon_path, :strip_image_path, :thumbnail_path. Missing files are silently skipped. new/1 is a struct! wrapper.

WalletPasses.Google.Visual :background_color (#RRGGBB, written to hexBackgroundColor verbatim), :logo_uri, :hero_image_uri (HTTPS), :wide_logo_uri (reserved, not emitted), :image_modules (list of {uri, content_description} tuples, defaults to []). new/1 is a struct! wrapper.

  • Getting Started for the full build pipeline.
  • Apple Wallet for the .pkpass bundle layout and how image files are packaged and signed.
  • Google Wallet for hero/logo image hosting conventions and class vs object visuals.
  • Localization for per-locale Apple images and Google content-description translation.
  • Pass Types for which image slot is the "primary" visual on each pass type.