Show artwork
Apple doesn't send an image URL for artwork. It sends an artwork template: a URL such as https://…/{w}x{h}bb.jpg, with the pixel size left for you to fill in. In this guide you turn one into an <img> that is sharp on a dense screen and laid out at the right size.
Before you start, you need:
- a resource with its attributes, such as a song or an album you fetched with a client.
The artwork functions come from @open-music-sdk/browser, @open-music-sdk/react and @open-music-sdk/server alike. They run anywhere: in a page, on a server, or wherever you write HTML. They touch neither the page nor the network.
In the code below, song stands for a song you fetched with a client.
1. Check that the resource has artwork
Anywhere, read the artwork from the resource's attributes:
const artwork = song.attributes?.artwork;
// { url: "https://…/{w}x{h}bb.jpg", width: 3000, height: 3000,
// bgColor: "…", … }Some resources have no artwork, and a resource can come without its attributes. The functions take an artwork, not the lack of one, so check before you call them. The one-line examples below leave the check out.
width and height are the largest the image comes, in pixels. The artwork also carries colours you can use while the image loads: bgColor and textColor1 to textColor4, each a hex colour without the #.
2. Fill in an <img> with artworkImage
In your page, where img is an <img> element, give artworkImage the artwork and the width you show the image at, in CSS pixels:
import { artworkImage } from "@open-music-sdk/browser";
if (artwork) Object.assign(img, artworkImage(artwork, 300));
// img.src https://…/300x300bb.jpg
// img.srcset https://…/300x300bb.jpg 1x, https://…/600x600bb.jpg 2x,
// https://…/900x900bb.jpg 3x
// img.width 300
// img.height 300In a component, give artworkImage the artwork and the width you show the image at, in CSS pixels. React spells the attribute srcSet:
import { artworkImage, type tSong } from "@open-music-sdk/react";
export function Cover({ song }: { song: tSong }) {
const artwork = song.attributes?.artwork;
if (!artwork) return null;
const { srcset, ...image } = artworkImage(artwork, 300);
return <img {...image} srcSet={srcset} alt={song.attributes?.name} />;
}artworkImage works out src, srcset, width and height together, and they have to agree. A browser takes src as the 1x image wherever the srcset doesn't offer one, and lays out an image that has no width and height by the density of whichever candidate it chose. Use it unless you need only one of the parts.
The vanilla sample draws its 240-pixel covers with one function, where h(tag, props) is a helper of its own that makes an element with those properties set. A resource with no artwork gets an empty box of the same size, so a page laid out around covers keeps its shape:
import { artworkImage, type tArtwork } from "@open-music-sdk/browser";
import { h } from "./dom";
/**
* An `<img>` for an artwork shown `size` CSS pixels wide, with a `srcset` for
* screens of higher density. A resource with no artwork gets an empty box of
* the same size.
*/
export function cover(
artwork: tArtwork | undefined,
size: number,
): HTMLImageElement {
if (artwork === undefined) {
return h("img", { alt: "", width: size, height: size });
}
return h("img", { alt: "", ...artworkImage(artwork, size) });
}The React sample draws its 240-pixel covers with one component. A resource with no artwork gets an empty box of the same size, so a page laid out around covers keeps its shape:
import { artworkImage, type tArtwork } from "@open-music-sdk/react";
interface tCoverProps {
readonly artwork: tArtwork | undefined;
/** How wide the cover is shown, in CSS pixels. */
readonly size: number;
/** Loaded only once it nears the screen. */
readonly lazy?: boolean;
}
/**
* An `<img>` for an artwork shown `size` CSS pixels wide, with a `srcSet` for
* screens of higher density. A resource with no artwork gets an empty box of
* the same size.
*/
export function Cover({ artwork, size, lazy = false }: tCoverProps) {
const loading = lazy ? "lazy" : undefined;
if (artwork === undefined) {
return <img alt="" width={size} height={size} loading={loading} />;
}
// React spells the attribute srcSet.
const { srcset, ...image } = artworkImage(artwork, size);
return <img alt="" {...image} srcSet={srcset} loading={loading} />;
}In a Next.js app, you can draw artwork with next/image instead, through a component you can take into your app: see With next/image.
How the size is chosen:
- With no
heightoption, the image keeps the artwork's own shape: a 1920 by 1080 still shown at 320 is asked for as320x180. Artwork that doesn't say how large it comes is taken to be square. - No image is asked for larger than the artwork comes. A box that asks for more shrinks in its own shape. No side is asked for past 10,000 pixels, where Apple's image server stops.
widthandheightare the size the image is shown at, in whole CSS pixels, whatever resolution was fetched.
3. Get one URL with artworkUrl
Anywhere, artworkUrl gives the URL of the image at one width, the src alone:
import { artworkUrl } from "@open-music-sdk/server";
const url = artworkUrl(artwork, 1200); // https://…/1200x1200bb.jpgUse it where you need a single URL, such as an image for a link preview.
All three functions give the same URL, character for character. It is spelled the way a browser reads a src: white space dropped from its ends, tabs and line breaks dropped from anywhere, and a space inside written %20. None of this changes what is requested, so a check you make on the URL from artworkUrl holds for the srcset too.
4. Build a srcset alone with artworkSrcSet
In your page, where img is an <img> element, artworkSrcSet gives the srcset alone. Set the image's width and height yourself:
import { artworkSrcSet } from "@open-music-sdk/browser";
if (artwork) {
img.srcset = artworkSrcSet(artwork, 300, { densities: [1, 2] });
img.width = 300;
img.height = 300;
}In a component, artworkSrcSet gives the srcset alone. Set the image's width and height yourself:
import { artworkSrcSet, type tSong } from "@open-music-sdk/react";
export function Cover({ song }: { song: tSong }) {
const artwork = song.attributes?.artwork;
if (!artwork) return null;
const srcSet = artworkSrcSet(artwork, 300, { densities: [1, 2] });
return (
<img
srcSet={srcSet}
width={300}
height={300}
alt={song.attributes?.name}
/>
);
}Each candidate is the image at the width times a density, labelled with that density. densities takes up to 16 numbers, and defaults to [1, 2, 3].
Where the artwork doesn't come large enough for a density, the largest image it has is offered once, under the density it amounts to. A 500-pixel cover shown at 300 gives …/300x300bb.jpg 1x, …/500x500bb.jpg 1.67x, so the browser doesn't download the same image twice or mistake its size.
That is why you set width and height. A 200-pixel cover shown at 300 has the srcset…/200x200bb.jpg 0.667x. Without width and height, a browser lays that image out 200 pixels wide, not 300.
A URL with no size to fill in, such as a fixed URL for a listener's own playlist artwork, is one image of unknown size. It comes back alone, with no density.
5. Choose the format and the crop
Anywhere, format and crop write over what the template names. A template's file name says both: {w}x{h}bb.jpg is cut bb and encoded jpg.
artworkUrl(artwork, 300, { format: "webp" }); // https://…/300x300bb.webpformat is one of "jpg", "jpeg", "png", "webp", "heic" or "heif", the formats Apple's image server was seen to convert to. For the one cover measured, WebP was a third of the JPEG's bytes.
crop is Apple's code for how the image is cut to the box, such as the bb most templates name. Apple doesn't document the codes, so any code of the right shape is accepted: see tArtworkOptions.
To fit artwork into a box of another shape, pass height as well:
const image = artworkImage(artwork, 1200, { height: 300 });What you get depends on the crop:
- With
bb, the whole image stands inside the box, never enlarged. A square cover in a 1200 by 300 slot is laid out 300 by 300. Don't set the box's own size on the<img>, or the image is stretched. - With
sr, the image fills the box exactly, enlarged if it must be. A code the SDK doesn't know is taken to do the same. - With
cc, it's a square as long as the longer of the two sides, enlarged if it must be.
Some templates leave the crop and the format open, as {c} and {f}. They're filled in the same way, with bb and jpg if you don't say. A URL that isn't a template of this form, such as a fixed URL for a listener's own playlist artwork, is returned as it is.
Apple documents {w}x{h} and nothing else. The rest is what its image server was seen to do: see Differences from Apple's documentation.
6. Check where the URL points
The functions fill in the URL the API gave, and don't look at where it points unless you ask. That is enough for artwork that came from Apple's API a moment ago.
Artwork from anywhere else can point anywhere
Artwork from a cache, a database that others can write to, a shared link or another service can hold any URL. Pass hosts for it. URLs in Apple's answers says what a bad URL can do.
Anywhere, pass the hosts the artwork may come from:
artworkImage(artwork, 300, { hosts: ["mzstatic.com"] });With hosts, a URL that isn't https on one of those hosts or a subdomain of one is a TypeError, and no URL is returned. Apple serves catalog artwork from is1-ssl.mzstatic.com and its numbered siblings. Artwork a listener gave a playlist of their own may come from elsewhere, so look at what your app receives before you settle the list.
What comes back is a URL, not markup. Assigned to an element, or given to React, it's escaped for you. Written into HTML by hand, escape it like any other attribute value, and don't put it inside a CSS url(…) as it is.
With next/image
In a Next.js app, next/image can take artworkUrl as its loader. It asks the loader for each width its srcset offers, and Apple's image server makes each one, so Next's own optimizer is never used and your Next config needs no remotePatterns for Apple. The component below, Artwork, does this. It comes from the Next sample and uses nothing else of it, so save it in your app as artwork.tsx, as it is:
"use client";
// Apple Music artwork through next/image. Nothing here is the sample's own, so
// the file can be taken into another app as it is.
//
// A client component, since a loader is a function, which a server component
// cannot hand to the page. A server component can still render it.
import {
artworkUrl,
type tArtwork,
type tArtworkFormat,
} from "@open-music-sdk/react";
import Image, { type ImageLoader } from "next/image";
import type { ComponentProps } from "react";
/**
* What next/image takes, but the props the artwork decides and those only
* Next's own optimizer, which is never used, would read.
*/
type tImageProps = Omit<
ComponentProps<typeof Image>,
"src" | "loader" | "quality" | "unoptimized"
>;
export interface tArtworkProps extends tImageProps {
/** A resource's `attributes.artwork`. With none, the box is left empty. */
readonly artwork: tArtwork | undefined;
/** The file format, such as `"webp"`. Default: the one the URL names. */
readonly format?: tArtworkFormat | undefined;
/**
* The hosts the artwork may come from, such as `["mzstatic.com"]`. Pass it
* for artwork that didn't come straight from Apple's API.
*/
readonly hosts?: readonly string[] | undefined;
}
/**
* An artwork laid out as next/image lays out any image: `width` CSS pixels
* wide, or filling its parent with `fill`. Without a `height`, the artwork's
* own shape decides it.
*
* next/image asks the loader for each width its `srcset` offers, and Apple's
* image server makes each one, so Next's own optimizer is never used. The
* artwork's own colour fills the box while the image loads.
*/
export function Artwork({
artwork,
format,
hosts,
...props
}: tArtworkProps) {
const width = props.width === undefined ? undefined : Number(props.width);
const height = props.height ?? heightOf(artwork, width);
// An image that fills its parent takes no height, so it is given none.
const size = height === undefined ? {} : { height };
if (artwork === undefined) {
return <Image {...props} {...size} src={BLANK} />;
}
const options = { format, hosts };
const loader: ImageLoader = ({ width }) =>
artworkUrl(artwork, width, options);
// A URL with no size to fill in, as the artwork of a listener's own
// playlist has, is one image at every width: next/image is told so, rather
// than handed a loader that cannot honour a width. It then uses `src` as it
// is, so that comes from artworkUrl too, where `hosts` is checked. Any width
// gives the same URL.
const sized = SIZED.test(artwork.url);
return (
<Image
{...props}
{...size}
loader={loader}
unoptimized={!sized}
src={sized ? artwork.url : artworkUrl(artwork, 1, options)}
style={{ backgroundColor: colourOf(artwork), ...props.style }}
/>
);
}
/**
* Whether a URL has a size to fill in, as artworkUrl reads one: `{w}` or
* `{h}`, with the braces percent-encoded or not.
*/
const SIZED = /\{[wh]\}|%7[Bb][wh]%7[Dd]/;
/** An image of nothing, for a box with no artwork to show. */
const BLANK = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>";
/**
* The height that keeps the artwork's own shape at `width`: `width` itself
* where the artwork doesn't say how large it comes, and none with no width.
*/
function heightOf(
artwork: tArtwork | undefined,
width: number | undefined,
): number | undefined {
if (width === undefined) return undefined;
if (artwork === undefined || !(artwork.width > 0 && artwork.height > 0)) {
return width;
}
return Math.round((width * artwork.height) / artwork.width);
}
/**
* The artwork's average colour, as CSS. It is checked, as the server writes
* a style into the page's HTML as it is: anything but six hex digits could
* add declarations of its own.
*/
function colourOf({ bgColor }: tArtwork): string | undefined {
if (bgColor === undefined) return undefined;
return /^[0-9a-f]{6}$/i.test(bgColor) ? `#${bgColor}` : undefined;
}In any component, server or client, give Artwork a resource's artwork and the width you show it at:
import type { tAlbum } from "@open-music-sdk/react";
import { Artwork } from "./artwork";
export function AlbumHeading({ album }: { album: tAlbum }) {
const { name, artwork } = album.attributes ?? {};
return (
<header>
<Artwork artwork={artwork} width={240} alt="" loading="eager" />
<h1>{name}</h1>
</header>
);
}The alt is empty because the heading beside the cover names the album. A cover on its own takes the name as its alt, as any image does.
Artwork takes:
artwork: a resource'sattributes.artwork. With none, it draws an empty image in the same box, so a page laid out around covers keeps its shape. You don't need to check for artwork first.widthandheight, asnext/imagetakes them. Leave outheightand the artwork's own shape decides it: a 1920 by 1080 still shown 320 wide is 180 high.fillandsizes, asnext/imagetakes them, for a box sized by CSS.format, such as"webp", as in step 5.hosts, the hosts the URL may point to, as in step 6.- Everything else
next/imagetakes, such asclassName,loadingandpreload. The exceptions aresrcandloader, whichArtworksets, andqualityandunoptimized, which only Next's own optimizer would use.
While the image loads, the box shows the artwork's bgColor.
For a cover that fills a grid cell, give the cell a position and a shape, and say how wide it is shown in sizes:
<div style={{ position: "relative", aspectRatio: 1 }}>
<Artwork
artwork={artwork}
fill
sizes="(max-width: 640px) 50vw, 240px"
alt={name}
/>
</div>How next/image and Artwork behave, as seen in Next 16 on 20 October 2026:
- Next picks the widths from its own list, not from the width you show the image at. A cover shown at 240 asks for
256x256, labelled1x, and640x640, labelled2x: for each density, the first of Next'simageSizesanddeviceSizesat least that wide.artworkImageasks for exactly240x240and480x480. remotePatternsin your Next config doesn't apply under a loader of your own, so nothing checks where the URL points buthosts, as in step 6. PasshoststoArtworkfor artwork from anywhere but Apple's API.- A URL with no size to fill in, as the artwork of a listener's own playlist has, is the same image at every width. Next warns about a loader that can't honour a width, so
Artworkpassesunoptimizedfor such a URL. Next then calls no loader and usessrcas it is, soArtworkmakes thatsrcwithartworkUrltoo, wherehostsis checked. - The artwork's
bgColorgoes into a style that the server writes into the page's HTML without parsing it first.Artworkuses it only if it is six hex digits, as Apple sends it, so a colour from elsewhere can't add CSS declarations of its own.
When something goes wrong
An argument that isn't what the function takes is a TypeError, and so, with hosts, is an artwork whose URL isn't on one of them. The message says which argument was wrong, and for a number, what it was. No error shows the URL. Each function's reference, linked below, lists every case.
Next steps
- Play song previews, with the artwork of the track that is playing.
- The reference for
artworkImage,artworkUrl,artworkSrcSetand their options.