Skip to content
Writing

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.

Blurred Background ExampleThe 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:

Terminal
# npm
npm install blurhash react-blurhash sharp

# yarn
yarn add blurhash react-blurhash sharp

Here'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.

app/api/gethash/route.ts
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.

ts
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.

ts
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.

ts
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.

ts
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.

app/page.tsx
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:

components/BlurHash.tsx
'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 BlurHash

punch 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!