Building a Spotify Now Playing Widget with Next.js
Adding a "Now Playing" widget to your portfolio is a great way to showcase your personality and technical skills. In this guide, I'll show you how to integrate Spotify's Web API with Next.js to display your currently playing music in real-time.
What We'll Build
A beautiful, animated widget that:
- Displays your currently playing Spotify track
- Shows album artwork, song title, and artist
- Updates automatically every 30 seconds
- Features smooth animations and dark mode support
- Links directly to the song on Spotify
Prerequisites
Before we start, you'll need:
- A Next.js 14+ project with TypeScript
- A Spotify account (Free or Premium)
- Basic knowledge of React and API routes
- Tailwind CSS and Framer Motion installed
Step 1: Set Up Spotify Developer Account
First, we need to create a Spotify application to get API credentials:
- Go to the Spotify Developer Dashboard
- Click "Create app"
- Fill in the details:
- App name: Your Portfolio
- Redirect URI:
http://localhost:3000/callback - Check the Web API checkbox
- Save and note your Client ID and Client Secret
Step 2: Get Your Refresh Token
To access your currently playing track, you need a refresh token with the right scope:
- Create an authorization URL (replace
YOUR_CLIENT_ID):
1https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost:3000/callback&scope=user-read-currently-playing- Open this URL in your browser and authorize the app
- Copy the
codeparameter from the redirect URL - Exchange it for a refresh token using curl:
1curl -X POST https://accounts.spotify.com/api/token \
2 -H "Content-Type: application/x-www-form-urlencoded" \
3 -d "grant_type=authorization_code" \
4 -d "code=YOUR_CODE" \
5 -d "redirect_uri=http://localhost:3000/callback" \
6 -d "client_id=YOUR_CLIENT_ID" \
7 -d "client_secret=YOUR_CLIENT_SECRET"- Save the
refresh_tokenfrom the response
Step 3: Create TypeScript Types
Create src/types/spotify.ts to define our data structures:
1export interface SpotifyNowPlayingData {
2 isPlaying: boolean;
3 title?: string;
4 artist?: string;
5 album?: string;
6 albumImageUrl?: string;
7 songUrl?: string;
8}
9
10export interface SpotifyTokenResponse {
11 access_token: string;
12 token_type: string;
13 expires_in: number;
14 scope: string;
15}
16
17export interface SpotifyCurrentlyPlayingResponse {
18 is_playing: boolean;
19 item?: {
20 name: string;
21 artists: Array<{ name: string }>;
22 album: {
23 name: string;
24 images: Array<{ url: string; height: number; width: number }>;
25 };
26 external_urls: { spotify: string };
27 };
28}Step 4: Create Spotify Utility Functions
Create src/utils/spotify.ts for API interactions:
1import { SpotifyTokenResponse } from "@/types/spotify";
2
3const client_id = process.env.SPOTIFY_CLIENT_ID;
4const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
5const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN;
6
7const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
8const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
9const NOW_PLAYING_ENDPOINT = "https://api.spotify.com/v1/me/player/currently-playing";
10
11export const getAccessToken = async (): Promise<SpotifyTokenResponse> => {
12 const response = await fetch(TOKEN_ENDPOINT, {
13 method: "POST",
14 headers: {
15 Authorization: `Basic ${basic}`,
16 "Content-Type": "application/x-www-form-urlencoded",
17 },
18 body: new URLSearchParams({
19 grant_type: "refresh_token",
20 refresh_token: refresh_token!,
21 }),
22 });
23
24 return response.json();
25};
26
27export const getNowPlaying = async (): Promise<Response> => {
28 const { access_token } = await getAccessToken();
29
30 return fetch(NOW_PLAYING_ENDPOINT, {
31 headers: {
32 Authorization: `Bearer ${access_token}`,
33 },
34 });
35};Step 5: Create the API Route
Create src/app/api/spotify/now-playing/route.ts:
1import { NextResponse } from "next/server";
2import { getNowPlaying } from "@/utils/spotify";
3import { SpotifyNowPlayingData } from "@/types/spotify";
4
5export const dynamic = "force-dynamic";
6
7export async function GET() {
8 try {
9 const response = await getNowPlaying();
10
11 if (response.status === 204 || response.status > 400) {
12 return NextResponse.json<SpotifyNowPlayingData>({ isPlaying: false });
13 }
14
15 const song = await response.json();
16
17 if (!song.item) {
18 return NextResponse.json<SpotifyNowPlayingData>({ isPlaying: false });
19 }
20
21 return NextResponse.json<SpotifyNowPlayingData>({
22 isPlaying: song.is_playing,
23 title: song.item.name,
24 artist: song.item.artists.map((a) => a.name).join(", "),
25 album: song.item.album.name,
26 albumImageUrl: song.item.album.images[0]?.url,
27 songUrl: song.item.external_urls.spotify,
28 });
29 } catch (error) {
30 return NextResponse.json<SpotifyNowPlayingData>(
31 { isPlaying: false },
32 { status: 500 }
33 );
34 }
35}Step 6: Build the React Component
Create src/components/SpotifyNowPlaying.tsx:
1"use client";
2
3import { useEffect, useState } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import { Music2 } from "lucide-react";
6import { SpotifyNowPlayingData } from "@/types/spotify";
7
8const SpotifyNowPlaying = () => {
9 const [data, setData] = useState<SpotifyNowPlayingData | null>(null);
10 const [loading, setLoading] = useState(true);
11
12 useEffect(() => {
13 const fetchNowPlaying = async () => {
14 try {
15 const response = await fetch("/api/spotify/now-playing");
16 const nowPlaying: SpotifyNowPlayingData = await response.json();
17 setData(nowPlaying);
18 } catch (error) {
19 console.error("Error fetching now playing:", error);
20 } finally {
21 setLoading(false);
22 }
23 };
24
25 fetchNowPlaying();
26 const interval = setInterval(fetchNowPlaying, 30000);
27
28 return () => clearInterval(interval);
29 }, []);
30
31 if (loading) {
32 return (
33 <div className="animate-pulse flex items-center gap-3">
34 <div className="w-12 h-12 bg-gray-300 dark:bg-gray-700 rounded" />
35 <div className="flex-1">
36 <div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2" />
37 <div className="h-3 bg-gray-300 dark:bg-gray-700 rounded w-1/2" />
38 </div>
39 </div>
40 );
41 }
42
43 if (!data?.isPlaying) {
44 return (
45 <div className="flex items-center gap-3">
46 <Music2 className="w-5 h-5 text-gray-500" />
47 <span className="text-sm text-gray-600">Not playing anything</span>
48 </div>
49 );
50 }
51
52 return (
53 <AnimatePresence mode="wait">
54 <motion.a
55 href={data.songUrl}
56 target="_blank"
57 rel="noopener noreferrer"
58 whileHover={{ scale: 1.02 }}
59 className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-lg border border-green-200 dark:border-green-800"
60 >
61 {data.albumImageUrl && (
62 <img
63 src={data.albumImageUrl}
64 alt={data.album}
65 className="w-12 h-12 rounded"
66 />
67 )}
68 <div className="flex-1 min-w-0">
69 <p className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
70 {data.title}
71 </p>
72 <p className="text-xs text-gray-600 dark:text-gray-400 truncate">
73 {data.artist}
74 </p>
75 </div>
76 </motion.a>
77 </AnimatePresence>
78 );
79};
80
81export default SpotifyNowPlaying;Step 7: Add Environment Variables
Create a .env.local file in your project root:
1SPOTIFY_CLIENT_ID=your_client_id_here
2SPOTIFY_CLIENT_SECRET=your_client_secret_here
3SPOTIFY_REFRESH_TOKEN=your_refresh_token_here⚠️ Important: Never commit
.env.localto version control!
Step 8: Use the Component
Import and use the component anywhere in your app:
1import SpotifyNowPlaying from "@/components/SpotifyNowPlaying";
2
3export default function Home() {
4 return (
5 <div>
6 <h1>Welcome to my portfolio</h1>
7 <SpotifyNowPlaying />
8 </div>
9 );
10}Customization Ideas
Here are some ways to enhance your widget:
- Add an animated equalizer when music is playing
- Show listening history using the recently played endpoint
- Display top tracks from your Spotify profile
- Add playback controls (requires additional scopes)
- Create a full music dashboard with stats and insights
Troubleshooting
Widget shows "Not playing anything":
- Verify your environment variables are correct
- Make sure you're actively playing music on Spotify
- Check that your refresh token has the
user-read-currently-playingscope
API returns 401 Unauthorized:
- Your refresh token may have expired - regenerate it
- Double-check your Client ID and Client Secret
Widget not updating:
- Check the browser console for errors
- Verify the API route is accessible at
/api/spotify/now-playing
Performance Considerations
- The widget polls every 30 seconds - adjust based on your needs
- Consider implementing SWR or React Query for better caching
- Use Next.js Image component for optimized album art loading
- Add error boundaries to prevent crashes
Conclusion
You now have a fully functional Spotify "Now Playing" widget! This is a great addition to any portfolio, showing both your technical skills and personality.
The same pattern can be applied to other APIs like:
- Last.fm for music scrobbling
- GitHub for recent activity
- Wakatime for coding stats
- Strava for fitness activities
Happy coding, and enjoy sharing your music taste with the world! 🎵