# Theming & Visual Design
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.
```json
{
"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.
```json
{
"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:
```elixir
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
```elixir
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:
```elixir
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:
| Field | Filename in `.pkpass` | Required by Apple? |
|-----------------------|------------------------|--------------------|
| `:icon_path` | `icon.png` | Yes |
| `:strip_image_path` | `strip.png` | Pass-type specific |
| `:thumbnail_path` | `thumbnail.png` | Optional |
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 passes** — `logo.png` plus `footer.png`; no strip.
- **Store cards / loyalty** — `strip.png` or `background.png`.
- **Coupons** — `strip.png`.
- **Generic** — `thumbnail.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][pkpass-ref] for the canonical
numbers. Per-locale image variants live in `<locale>.lproj/` — see
[Localization](localization.md).
[pkpass-ref]: https://developer.apple.com/library/archive/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html
## Google image fields
`WalletPasses.Google.Visual` carries URIs (not paths) for the images
Google's servers fetch:
| Field | Google JSON path | Notes |
|--------------------|----------------------------------------|---------------------------------------|
| `:logo_uri` | `logo.sourceUri.uri` | Square logo, top-left of pass face |
| `:hero_image_uri` | `heroImage.sourceUri.uri` | Wide banner across the top |
| `:wide_logo_uri` | (struct-only, reserved) | Not currently emitted into JSON |
| `:image_modules` | `imageModulesData[].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](localization.md) 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][google-images]; logos render in a
small square, hero images render full-width across the pass top.
[google-images]: https://developers.google.com/wallet/generic/resources/style-guidelines
For longer passes that need additional inline imagery (event poster,
venue map, sponsor logo), use `:image_modules`:
```elixir
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](lifecycle.md)) 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.
### Related guides
- [Getting Started](getting-started.md) for the full build pipeline.
- [Apple Wallet](apple-wallet.md) for the `.pkpass` bundle layout and
how image files are packaged and signed.
- [Google Wallet](google-wallet.md) for hero/logo image hosting
conventions and class vs object visuals.
- [Localization](localization.md) for per-locale Apple images and
Google content-description translation.
- [Pass Types](pass-types.md) for which image slot is the "primary"
visual on each pass type.