Spotify listening widgets for developer portfolios.
React · Vanilla JS · Playback · Theming · Discussions
npm install @tastify/reactimport { TastifyProvider, NowPlaying, TopTracks, TopAlbums } from '@tastify/react'
import '@tastify/react/styles.css'
function App() {
return (
<TastifyProvider tokenUrl="/api/spotify/token">
<NowPlaying />
<TopTracks limit={5} />
<TopAlbums limit={5} />
</TastifyProvider>
)
}Full API reference in the
@tastify/reactREADME.
<link rel="stylesheet" href="https://unpkg.com/@tastify/vanilla/dist/styles.css" />
<div data-tastify="now-playing"></div>
<div data-tastify="top-tracks" data-limit="5"></div>
<div data-tastify="top-albums" data-limit="5"></div>
<div data-tastify="recently-played"></div>
<script src="https://unpkg.com/@tastify/vanilla" data-tastify-token-url="/api/spotify/token"></script>No build step or framework needed.
Full API reference in the
@tastify/vanillaREADME.
Spotify doesn't allow client-side token refresh (the client secret would be exposed). You need a small server-side endpoint that exchanges your refresh token for a short-lived access token. The CLI generates this for you:
npx @tastify/cli initIt walks you through Spotify OAuth, creates a serverless function for your platform (Vercel, Netlify, Cloudflare Workers, or Express), and writes your .env.local with the right credentials. Takes about 30 seconds.
The CLI writes your credentials to .env.local for local development, but in production you need to set the same three variables in your platform's environment variable settings — they are never read from .env.local at runtime.
| Variable | Where to set it |
|---|---|
SPOTIFY_CLIENT_ID |
Platform dashboard (Vercel / Netlify / Cloudflare / etc.) |
SPOTIFY_CLIENT_SECRET |
Platform dashboard |
SPOTIFY_REFRESH_TOKEN |
Platform dashboard |
Platform notes
- Vercel — function goes in
api/spotify/token.ts. Environment variables set in Project Settings → Environment Variables. - Netlify — function goes in
netlify/functions/spotify-token.ts. Variables set in Site Settings → Environment Variables. - Cloudflare Pages — function goes in
functions/api/spotify/token.tsat the root of your repository (not inside a subdirectory). Variables set in Pages project → Settings → Environment Variables. Note: if your project root is set to a subdirectory, Cloudflare looks forfunctions/relative to that subdirectory. - Express — set variables in your host's environment or a
.envfile loaded bydotenv.
Verifying the endpoint
Before testing your components, confirm the token endpoint is working by visiting it directly in your browser:
https://your-site.example.com/api/spotify/token
It should return JSON with an access_token field. If it returns an error or HTML, the function isn't deployed correctly or the credentials aren't set.
| Package | Description | Docs |
|---|---|---|
@tastify/core |
Spotify API client, caching, polling, playback engine | README |
@tastify/react |
React components, hooks, and playback UI | README |
@tastify/vanilla |
Zero-framework widgets + data-attribute auto-init | README |
@tastify/cli |
npx @tastify/cli init — scaffolds your token endpoint |
— |
Shows what you're currently listening to, with album art and a live progress bar. Polls every 15s by default.
<NowPlaying compact showProgress fallback={<p>Not playing</p>} />| Prop | Type | Default | Description |
|---|---|---|---|
pollInterval |
number |
15000 |
Polling interval in ms |
showArt |
boolean |
true |
Display album artwork |
showProgress |
boolean |
true |
Show progress bar |
interactive |
boolean |
true |
Click to play (requires PlaybackProvider) |
compact |
boolean |
false |
Compact layout |
fallback |
ReactNode |
— | Shown when nothing is playing |
className |
string |
— | Custom class name |
children |
(data) => ReactNode |
— | Headless render prop |
Your most-played tracks over a configurable time range.
<TopTracks timeRange="short_term" limit={10} layout="grid" columns={3} />| Prop | Type | Default | Description |
|---|---|---|---|
timeRange |
'short_term' | 'medium_term' | 'long_term' |
'medium_term' |
Spotify time range |
limit |
number |
20 |
Number of tracks |
layout |
'list' | 'grid' |
'list' |
Layout mode |
showRank |
boolean |
true |
Show rank numbers |
showArt |
boolean |
true |
Display artwork |
columns |
number |
— | Grid column count |
header |
string |
— | Section heading |
showTimeRangeSelector |
boolean |
false |
Interactive time range tabs |
className |
string |
— | Custom class name |
children |
(data) => ReactNode |
— | Headless render prop |
Your top artists with optional genre tags.
<TopArtists limit={6} layout="grid" columns={3} showGenres />| Prop | Type | Default | Description |
|---|---|---|---|
timeRange |
'short_term' | 'medium_term' | 'long_term' |
'medium_term' |
Spotify time range |
limit |
number |
20 |
Number of artists |
layout |
'grid' | 'list' |
'grid' |
Layout mode |
columns |
number |
— | Grid column count |
showGenres |
boolean |
false |
Show genre tags |
header |
string |
— | Section heading |
showTimeRangeSelector |
boolean |
false |
Interactive time range tabs |
className |
string |
— | Custom class name |
children |
(data) => ReactNode |
— | Headless render prop |
Your most-played albums over a configurable time range.
<TopAlbums timeRange="short_term" limit={10} layout="grid" columns={3} />| Prop | Type | Default | Description |
|---|---|---|---|
timeRange |
'short_term' | 'medium_term' | 'long_term' |
'medium_term' |
Spotify time range |
limit |
number |
5 |
Number of albums |
layout |
'list' | 'grid' | 'compact-grid' |
'list' |
Layout mode |
showRank |
boolean |
true |
Show rank numbers |
showArt |
boolean |
true |
Display artwork |
columns |
number |
— | Grid column count |
header |
string |
— | Section heading |
showTimeRangeSelector |
boolean |
false |
Interactive time range tabs |
className |
string |
— | Custom class name |
children |
(data) => ReactNode |
— | Headless render prop |
A timeline or list of recently played tracks.
<RecentlyPlayed limit={10} layout="timeline" showTimestamp />| Prop | Type | Default | Description |
|---|---|---|---|
limit |
number |
20 |
Number of tracks |
layout |
'list' | 'timeline' |
'list' |
Layout mode |
showTimestamp |
boolean |
true |
Show relative timestamps |
groupByDay |
boolean |
false |
Group items by day |
header |
string |
— | Section heading |
className |
string |
— | Custom class name |
children |
(data) => ReactNode |
— | Headless render prop |
tastify ships with a built-in audio engine that lets visitors play track previews directly on your site. Three playback modes are available:
| Mode | Requires Premium | Auth Required | Fidelity |
|---|---|---|---|
'embed' (default) |
No | No | ~30s previews via Spotify IFrame |
'preview' |
No | No | ~30s preview URLs |
'sdk' |
Yes | Yes | Full-length tracks via Web Playback SDK |
Embed mode uses Spotify's IFrame API to stream ~30-second previews. It requires no authentication from the visitor and no Premium subscription — any person who lands on your site can click play and hear music immediately. This makes it the best default for portfolio sites where you have zero control over who's visiting.
SDK mode connects to the Spotify Web Playback SDK, which registers your site as a Spotify Connect device (like a speaker or phone). This unlocks full-length track playback, but comes with trade-offs:
- Requires Spotify Premium — free-tier accounts can't use the Web Playback SDK.
- Takes over the active device — Spotify only streams to one device at a time. When SDK mode activates, playback transfers to the browser and stops on the visitor's phone/desktop app. If they start playing on another device, the browser session pauses.
- Requires a valid access token with the
streamingscope.
SDK mode is best suited for personal dashboards or sites where you are the primary visitor and want to use it as an actual player. For public-facing portfolio pages, stick with embed.
Use 'auto' to try SDK first and silently fall back to embed when the visitor doesn't have Premium.
Wrap your app with PlaybackProvider and drop in a PlaybackOverlay:
import {
TastifyProvider,
PlaybackProvider,
PlaybackOverlay,
TopTracks,
} from '@tastify/react'
import '@tastify/react/styles.css'
function App() {
return (
<TastifyProvider tokenUrl="/api/spotify/token">
<PlaybackProvider ui="bar">
<TopTracks limit={10} />
<PlaybackOverlay />
</PlaybackProvider>
</TastifyProvider>
)
}Click any track — the playback bar appears at the bottom of the viewport. Choose ui="toast" for a floating notification-style player instead.
See the
@tastify/reactplayback docs forusePlayback, bar vs. toast, SDK mode, and more.
import { mount, mountPlaybackBar } from '@tastify/vanilla'
const bar = mountPlaybackBar()
mount('#tracks', {
type: 'top-tracks',
tokenUrl: '/api/spotify/token',
onTrackPlay: (track) => bar.update(),
})See the
@tastify/vanillaplayback docs for toast mounts, embed vs. SDK, and configuration.
Both @tastify/react and @tastify/vanilla accept a theme value:
| Value | Behavior |
|---|---|
'light' |
Light surface/text colors (default) |
'dark' |
Dark surface/text colors |
'auto' |
Follows the visitor's prefers-color-scheme system setting |
In React, pass it to the provider:
<TastifyProvider tokenUrl="/api/spotify/token" theme="auto">In vanilla, set it per-widget or as a data-tf-theme attribute:
<div data-tastify="now-playing" data-tf-theme="dark"></div>mount('#player', { type: 'now-playing', tokenUrl: '...', theme: 'dark' })Every component is styled with CSS custom properties, so you can restyle anything without touching JS. Override them on .tastify-provider (React) or [data-tastify] (vanilla):
[data-tastify], .tastify-provider {
--tf-color-bg: #0a0a0a;
--tf-color-surface: #1a1a1a;
--tf-color-text-primary: #ffffff;
--tf-color-text-secondary: #a0a0a0;
--tf-color-accent: #1db954;
--tf-color-border: #2a2a2a;
--tf-radius: 12px;
--tf-art-size: 56px;
--tf-font-family: 'Inter', system-ui, sans-serif;
--tf-transition-speed: 200ms;
}| Variable | Controls |
|---|---|
--tf-color-bg |
Page/widget background |
--tf-color-surface |
Card and container surfaces |
--tf-color-text-primary |
Track names, headings |
--tf-color-text-secondary |
Artist names, timestamps, secondary text |
--tf-color-accent |
Progress bars, active states, links |
--tf-color-border |
Card and divider borders |
--tf-radius |
Border radius on cards and artwork |
--tf-art-size |
Album/artist artwork dimensions |
--tf-font-family |
Font stack for all widget text |
--tf-transition-speed |
Animation/transition duration |
These variables cascade, so you can scope overrides to individual widgets or breakpoints:
@media (max-width: 640px) {
.tastify-provider {
--tf-art-size: 40px;
--tf-radius: 8px;
}
}Every component accepts a children render prop for full control over markup:
<NowPlaying>
{(data) => (
<div>
<img src={data.track.album.images[0]?.url} alt="" />
<p>{data.track.name} — {data.track.artists[0]?.name}</p>
</div>
)}
</NowPlaying>In vanilla JS, use the mount() function with onTrackPlay / onArtistPlay callbacks for equivalent control.
- A Spotify account (Premium not required for most features)
- A registered Spotify Developer application
- Node 18+ for the CLI and serverless functions
- React 18+ for
@tastify/react
git clone https://github.com/cristobalwee/tastify.git
cd tastify
pnpm install
pnpm buildThe monorepo uses Turborepo + pnpm workspaces. Individual packages live under packages/ and can be built/tested independently with pnpm --filter @tastify/core build.
MIT — build something cool.
