Blogs

Building a real time Spotify "Now Playing" Component

28/03/2025

As a music enthusiast and developer, I wanted to share my current music taste with visitors to my personal website. In this blog post, I'll walk through how I built a real-time "Now Playing" Spotify integration for my Next.js website.

The Vision

I wanted a subtle but engaging component that would:

Technical Approach

The implementation required three main parts:

  1. Authentication with Spotify's API
  2. Fetching the currently playing track
  3. Creating a responsive React component

Let's break down each part of the solution.

1. Setting Up Spotify API Authentication

The first challenge was authentication. Spotify's API uses OAuth 2.0, which typically requires user interaction. Since I wanted this to work server-side without user input, I implemented the "Client Credentials Flow" with a refresh token.

This is being done in SpotifyAPI.ts file.

import queryString from "query-string";

const client_id = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_ID;
const client_secret = process.env.NEXT_PUBLIC_SPOTIFY_CLIENT_SECRET;
const refresh_token = process.env.NEXT_PUBLIC_SPOTIFY_REFRESH_TOKEN;

To avoid making unnecessary API calls, I implemented token caching:

// Cache structure to store the token and its expiration
let tokenCache = {
  access_token: null,
  expires_at: 0,
};

The getAccessToken function checks if there's a valid cached token before requesting a new one:

const getAccessToken = async () => {
  // Check if we have a cached token that's still valid
  if (
    tokenCache.access_token &&
    tokenCache.expires_at &&
    Date.now() < tokenCache.expires_at
  ) {
    return { access_token: tokenCache.access_token };
  }

  // If no valid token in cache, request a new one
  const concatenatedString = `${client_id}:${client_secret}`;
  const basic = Buffer.from(concatenatedString).toString("base64");

  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: queryString.stringify({
      grant_type: "refresh_token",
      refresh_token,
    }),
  });

  const data = await response.json();

  // Cache the new token with expiration
  // Spotify tokens typically expire in 1 hour (3600 seconds)
  // We subtract 60 seconds as a buffer
  tokenCache = {
    access_token: data.access_token,
    expires_at: Date.now() + (data.expires_in - 60) * 1000,
  };

  return { access_token: data.access_token };
};

2. Fetching the Currently Playing Track

Once authentication was sorted, I created a function to fetch the currently playing track:

export const getNowPlaying = async () => {
  const { access_token } = await getAccessToken();

  const response = await fetch(
    "https://api.spotify.com/v1/me/player/currently-playing",
    {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
      // Add cache control headers to prevent caching
      cache: "no-store",
      next: { revalidate: 0 },
    }
  );

  return response;
};

The important details here:

I then created a helper function to parse the response:

export default async function getNowPlayingItem() {
  const response = await getNowPlaying();
  if (response.status === 204 || response.status > 400) {
    return false;
  }

  const song = await response.json();
  const artist = song.item.artists[0].name;
  const isPlaying = song.is_playing;
  const title = song.item.name;

  return {
    artist,
    isPlaying,
    title,
  };
}

3. Building the React Component

The React component needed to:

  1. Display initial data from the server
  2. Periodically poll for updates
  3. Handle overflow with a scrolling animation
  4. Show different states based on playback status

First, I used a Spotify Logo SVG component:

const SpotifyLogo = ({
  width = 14,
  height = 14,
  color = "#a3a3a3",
}: {
  width?: number;
  height?: number;
  color?: string;
}) => (
  <svg width={width} height={height} x="0px" y="0px" viewBox="0 0 20 20">
    <g>
      <g>
        <g>
          <path
            fill={color}
            className="st0"
            d="M10,0C4.5,0,0,4.5,0,10c0,5.5,4.5,10,10,10c5.5,0,10-4.5,10-10C20,4.5,15.5,0,10,0z M14.6,14.4
                        c-0.2,0.3-0.6,0.4-0.9,0.2c-2.3-1.4-5.3-1.8-8.8-1c-0.3,0.1-0.7-0.1-0.7-0.5c-0.1-0.3,0.1-0.7,0.5-0.7c3.8-0.9,7.1-0.5,9.7,1.1
                        C14.7,13.7,14.8,14.1,14.6,14.4z M15.8,11.7c-0.2,0.4-0.7,0.5-1.1,0.3C12,10.3,8,9.8,4.8,10.8c-0.4,0.1-0.8-0.1-1-0.5
                        c-0.1-0.4,0.1-0.8,0.5-1c3.6-1.1,8.1-0.6,11.2,1.3C15.9,10.9,16,11.3,15.8,11.7z M15.9,8.9C12.7,7,7.4,6.8,4.3,7.7
                        c-0.5,0.1-1-0.1-1.2-0.6C3,6.6,3.3,6.1,3.8,5.9c3.5-1.1,9.4-0.9,13.1,1.3c0.4,0.3,0.6,0.8,0.3,1.3C16.9,9,16.4,9.1,15.9,8.9z"
          />
        </g>
      </g>
    </g>
  </svg>
);

export default SpotifyLogo;

Here's how I structured the actual component:

This is being done in SpotifyStatus.tsx file.

"use client";
import React, { useEffect, useState, useRef, ReactNode } from "react";
import SpotifyLogo from "./SpotifyLogo";

interface SpotifyData {
  artist: string;
  isPlaying: boolean;
  title: string;
}

const SpotifyStatus: React.FC<{
  initialData: SpotifyData | false,
  fallback?: ReactNode
}> = ({
  initialData,
  fallback
}) => {
  const [offline, setOffline] = useState<boolean>(!initialData);
  const [result, setResult] = useState<SpotifyData>(
    initialData || {
      artist: "",
      isPlaying: false,
      title: "",
    },
  );

Handling Text Overflow

One of the more interesting challenges was handling text overflow. I wanted long track names to scroll horizontally when they didn't fit. This required:

  1. References to measure the text and container widths
  2. An effect to detect overflow
  3. CSS animations to handle the scrolling
const textRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [shouldScroll, setShouldScroll] = useState(false);

// Unified Effect for Resize and Overflow Handling
useEffect(() => {
  const updateScrollBehavior = () => {
    if (textRef.current && containerRef.current) {
      const textWidth = textRef.current.scrollWidth;
      const containerWidth = containerRef.current.clientWidth;
      const isOverflowing = textWidth > containerWidth;
      setShouldScroll(isOverflowing);

      if (isOverflowing) {
        const translateX = -(textWidth - containerWidth) - 15;
        textRef.current.style.transform = `translateX(${translateX}px)`;
      } else {
        textRef.current.style.transform = "translateX(0)";
      }
    }
  };

  updateScrollBehavior();
  window.addEventListener("resize", updateScrollBehavior);

  return () => window.removeEventListener("resize", updateScrollBehavior);
}, [result]);

Periodic Updates

To keep the displayed track current, I implemented polling:

useEffect(() => {
  const interval = setInterval(async () => {
    try {
      const response = await fetch("/api/spotify/now-playing");
      const data = await response.json();

      if (!data || data.error) {
        setOffline(true);
      } else {
        setResult(data);
        setOffline(false);
      }
    } catch (error) {
      setOffline(true);
    }
  }, 15000);

  return () => clearInterval(interval);
}, []);

The text is handled separately:

// Hide component if offline or not playing, show fallback
if (offline || !result.isPlaying) {
  return fallback || null;
}

const scrollingText = `${result.title} by ${result.artist}`;

The Final JSX

The component's render output is deliberately minimal but expressive:

return (
    <div className="flex items-center gap-2">
      <div>
        <SpotifyLogo
          width={16}
          height={16}
          color={"#25d865"}
        />
      </div>
      <div ref={containerRef} className="w-full inline-block overflow-hidden">
        <div
          ref={textRef}
          className={`text-xs text-neutral-400 whitespace-nowrap ${
            shouldScroll ? "marquee" : ""
          }`}
        >
          <span>{scrollingText}</span>
        </div>
      </div>
    </div>
  );
};

Integration with Next.js App

Integrating the component into my Next.js footer was straightforward. I added a fallback though, that when no music is playing, it shows my name and year:

export default async function Footer() {
  const initialData = await getNowPlayingItem();
  //Other footer related things
  return (
    <footer className="flex flex-col justify-center m-4 gap-4 mt-32">
      {/* Other components */}
      <section className="text-neutral-400 text-xs">
        <SpotifyStatus
          initialData={initialData}
          fallback={
            <div>
              ({Data.version}) {Data.year}© {Data.firstName} {Data.lastName}.
            </div>
          }
        />
      </section>
    </footer>
  );
}

The key here is using initialData to provide server-rendered content before client-side JavaScript takes over.

CSS Animation

In my globals.css file, I added this animation:

.marquee {
  animation: scroll-left 10s linear infinite alternate;
}

@keyframes scroll-left {
  0% {
    transform: translateX(0%);
  }
  100% {
    transform: translateX(
      calc(-100% + 100% / (scrollWidth / 100%))
    ); /* calculate the translation value based on the scrollWidth */
  }
}

Final Result

Here are the two states of the component:


Playing State

Fig 1: Component when songs are playing


Not Playing State

Fig 2: Component when no songs are playing


You can see it in action down below in the footer as well.

Challenges and Solutions

Challenge 1: API Rate Limits

Spotify's API has rate limits, so I needed to ensure I wasn't making too many requests. I addressed this by:

Challenge 2: Cross-browser Animation Compatibility

Getting smooth scrolling animations working consistently across browsers was tricky. I found that using CSS variables for the animation distance worked best:

if (isOverflowing) {
  const translateX = -(textWidth - containerWidth) - 15;
  textRef.current.style.transform = `translateX(${translateX}px)`;
} else {
  textRef.current.style.transform = "translateX(0)";
}

Challenge 3: Server/Client Hydration

With Next.js, there's always the challenge of ensuring smooth hydration between server and client rendering. I solved this by:

Future Improvements

There are several ways this component can be enhanced according to personal preferences:

Conclusion

Building this Spotify integration was a fun project that adds a personal touch to my website. The combination of Next.js server components for initial data fetching and client components for real-time updates works beautifully.

Feel free to adapt this approach for your own projects!

Getting Started with Your Own Implementation

To implement this on your own site:

  1. Create a Spotify Developer account and register an application
  2. Obtain your client ID and client secret
  3. Generate a refresh token (several tutorials online can help with this)
  4. Set up your environment variables
  5. Clone or adapt the code shown in this post

Happy coding and happy listening!