5 min readupdated 2 days ago
How to create apple music's background blur effect
In this blog post, we'll learn how to create apple music's background blur effect using blurhash
If you've ever left a song playing in Apple Music, you've probably noticed the background. It's a soft blur that takes on the colours of the album art and shifts with every track. I found it oddly satisfying, and I wanted to know how hard it would be to build myself. Turns out it's not hard at all, and in this post we'll recreate it with blurhash.
The finished effect: the blur takes its colours from the image above it.
Getting started
I'll assume you already have a Next.js project. If you don't, the official guide takes about two minutes. Then install three packages:
# npm
npm install blurhash react-blurhash sharp
# yarn
yarn add blurhash react-blurhash sharpHere's what each one does:
- blurhash encodes an image into a short string that describes its blurred look.
- react-blurhash decodes that string and draws it on a canvas.
- sharp is a fast image-processing library. We use it to shrink the image before encoding.
Generating the hash
First we need an API route that takes an image URL and returns its blurhash. I'm using the App Router, but if you're on the Pages Router, the same logic works in an API route.
import * as blurhash from 'blurhash'
import { NextResponse } from 'next/server'
import sharp from 'sharp';
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const url = searchParams.get('url')
if (!url) return NextResponse.json({
message: 'Required fields is empty'
}, {
status: 400
})
try {
const image = await fetch(url);
const buffer = Buffer.from(await image.arrayBuffer())
const { data, info } = await sharp(buffer)
.raw()
.ensureAlpha()
.resize(32, 32)
.toBuffer({
resolveWithObject: true
});
const hash = blurhash.encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4,
4
)
return NextResponse.json({
hash
})
} catch (err) {
return NextResponse.json({
error: err
}, {
status: 404
})
}
}Route handler files
The file has to be named route.js or route.ts. A route handler and a page.js can't live at the same level of the folder tree, so keep them apart. The route handler docs cover the details.
Breaking the route down
Let's go through it one step at a time.
1. Validate the URL
The route takes the image URL as a query parameter, so the first job is to read it and bail out early if it's missing.
const { searchParams } = new URL(req.url)
const url = searchParams.get('url')
if (!url) return NextResponse.json({
message: 'Required fields is empty'
}, {
status: 400
})2. Fetch the image
Next we download the image and turn the response into a buffer, which is the format sharp works with.
const image = await fetch(url);
const buffer = Buffer.from(await image.arrayBuffer())3. Shrink it with sharp
We pass the buffer to sharp and resize it down to 32×32 pixels. A blurred image doesn't need detail, and a smaller image means a much faster encode.
const { data, info } = await sharp(buffer)
.raw()
.ensureAlpha()
.resize(32, 32)
.toBuffer({
resolveWithObject: true
});4. Encode the hash
The raw pixels go into blurhash's encode function, which produces the hash string. The last two arguments are how many colour components to use horizontally and vertically. Higher numbers capture more detail but make a longer string.
const hash = blurhash.encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)5. Return the hash
Finally, the route responds with the hash as JSON. That's the whole API.
Rendering the blur
With the endpoint working, the UI side is short. Fetch the hash on the server, then hand it to a small client component that draws it.
import Image from 'next/image'
import BlurHash from './components/BlurHash'
async function getBlurHash() {
const response = await fetch('http://localhost:3000/api/gethash?url=https://plus.unsplash.com/premium_photo-1685077715983-772598c45360?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=200')
return await response.json()
}
export default async function Home() {
const { hash } = await getBlurHash()
return (
<main className='main'>
<div className='hash'>
<BlurHash hash={hash} />
</div>
<Image src='https://plus.unsplash.com/premium_photo-1685077715983-772598c45360?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=986&q=80' alt='' width={500} height={500} />
</main>
)
}Don't hard-code localhost
That localhost:3000 address only works in development. In production, read the base URL from an environment variable, or skip the HTTP round trip and call the hashing function directly from your server component.
The Blurhash component from react-blurhash only runs in the browser, so it lives in its own client component:
'use client';
import React from 'react'
import { Blurhash } from 'react-blurhash';
const BlurHash = ({ hash }: { hash: string }) => {
return (
<Blurhash
hash={hash}
width={"100%"}
height={"100%"}
resolutionX={32}
resolutionY={32}
punch={1}
/>
)
}
export default BlurHashpunch controls how saturated the colours are, so raise it for a bolder background and lower it for something quieter. The resolutionX and resolutionY props set the size of the canvas it draws on. Because the result is blurred anyway, 32×32 is plenty.
Wrapping up
That's the whole trick: shrink the image, encode it to a hash, and draw the hash behind the image. You can find the full code in my repo. If you have questions or ideas, reach me on X at @jana__sundar or by email at mailtojana23@gmail.com. Until next time, happy coding!