A visually stunning React application that transforms Spotify Exportify CSV playlists into an interactive vinyl record player with Blade Runner/Iron Man HUD aesthetics.
- Glowing Upload Portal: Animated entry point with pulsing magenta gradient glow
- Breathing Animation: Upload icon rotates with halo glow effect
- Backdrop Blur: Ultra-glassmorphism with 64px blur (blur-3xl)
- Loading States: Rotating spinner with gradient border during CSV parsing
-
Pure CSS Vinyl Record:
- Rotating turntable with neon cyan glow
- SVG groove texture (12 concentric circles)
- Metallic reflection gradient overlay
- Animated center label with track info
- Pulsing indicator dot on label
- Bouncing needle arm during playback
-
Floating Controller Pill (Desktop):
- Symmetric 5-button layout: [Prev] [-5s] [Play/Pause] [+5s] [Next]
- Center play/pause button with breathing scale animation (1β1.05β1)
- MagentaβPink gradient with pulsing glow shadow
- Hover effects and disabled state styling
-
Ultra-Glassmorphism:
- 40px+ backdrop blur (
backdrop-blur-3xl) - Semi-transparent backgrounds (
rgba(15, 15, 35, 0.8)) - Neon purple/pink borders with inset shadow glow
- Subtle transparency overlays
- 40px+ backdrop blur (
-
Slide-Out Drawer:
- Desktop: Slides from right with
border-left-2 border-purple-500/30 - Mobile: Slides from bottom with
border-top-2 border-purple-500/30 - Glassmorphic panel with neon borders
- Desktop: Slides from right with
-
Pagination System:
- 10 tracks per page with prev/next controls
- Page indicator (e.g., "1-10 / 250")
- Active track highlighted with pink left border + pulsing indicator
- Smooth hover animations
-
Track Selection: Click any track to play instantly
- Cyber-Matrix Background:
- Deep slate-950βpurple-950βslate-950 gradient base
- 3 animated floating orbs (purple, blue, pink) with infinite motion
- Horizontal grid lines with gradient fades
- No standard grey/blue colors β pure cyberpunk aesthetic
- HTML5 Audio API: Full control over preview playback
- Seek Controls: -5s and +5s buttons for quick navigation
- Next/Prev: Navigate through playlist with disabled state at edges
- Auto-Advance: Automatically plays next track when current ends
- Responsive Preview URLs: Support for Spotify preview URLs
- Spotify Exportify Format: Parse CSV exports from Spotify
- Robust Handling: Quoted values, escaped commas, multi-line support
- Track Metadata: Extracts Track Name, Artist Names, Album, URI
- Preview URLs: Integrates with Spotify preview system
-
Desktop Layout (β₯768px):
- Centered vinyl turntable (320px diameter)
- Large track info display
- Floating controller pill
- Side-sliding playlist drawer
-
Mobile Layout (<768px):
- Fixed glassmorphic top navbar
- Compact mini controls (play/pause + playlist toggle)
- Bottom-sliding drawer
- Touch-optimized buttons
- Vinyl Rotation: Smooth 3s infinite spin during playback
- Breathing Glow: 2.5s pulse cycle on play button and portal glow
- Drawer Slide: Spring damping (30) + stiffness (300) for smooth entrance
- Icon Animations: Floating, rotating, and bouncing effects
- Track Hover: Stagger animations on playlist items
- Layout Transitions: Responsive design changes with smooth morphing
# Clone the repository
git clone https://github.com/samuelezranas/play-vinyl-csv.git
cd play-vinyl-csv
# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run buildUsage:
- Open the app: Navigate to
http://localhost:5177/ - Upload CSV: Click the glowing portal or drag a Spotify Exportify CSV file
- Play: Click the center play/pause button to start the vinyl
- Navigate: Use prev/next buttons to skip tracks
- Seek: Use Β±5s buttons to jump through tracks
- View Playlist: Click "Playlist βΊ" to see all tracks with pagination
import { PlayVinylCSV, parseSpotifyCSV } from 'play-vinyl-csv';
import { useState } from 'react';
function MyApp() {
const [tracks, setTracks] = useState([]);
const [currentTrackIndex, setCurrentTrackIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const handlePlayPause = () => setIsPlaying(!isPlaying);
const handleNext = () => {
if (currentTrackIndex < tracks.length - 1) {
setCurrentTrackIndex(prev => prev + 1);
}
};
return (
<PlayVinylCSV
tracks={tracks}
currentTrackIndex={currentTrackIndex}
isPlaying={isPlaying}
paginatedTracks={tracks.slice(0, 10)}
playlistPage={0}
totalPages={1}
showPlaylist={false}
isDesktop={true}
onPlayPause={handlePlayPause}
onNext={handleNext}
onPrev={() => setCurrentTrackIndex(Math.max(0, currentTrackIndex - 1))}
onSeekBack={() => {}}
onSeekForward={() => {}}
onSelectTrack={(index) => setCurrentTrackIndex(index)}
onTogglePlaylist={() => {}}
onPrevPage={() => {}}
onNextPage={() => {}}
onUpload={() => {}}
/>
);
}
export default MyApp;import { parseSpotifyCSV } from 'play-vinyl-csv';
async function uploadPlaylist(file) {
const csvText = await file.text();
const tracks = parseSpotifyCSV(csvText);
console.log('Parsed tracks:', tracks);
return tracks;
}// In your main app file:
import 'play-vinyl-csv/styles';| Prop | Type | Required | Description |
|---|---|---|---|
tracks |
Array | β | Array of track objects with {trackName, artistNames, album, previewUrl} |
currentTrackIndex |
Number | β | Index of currently playing track |
isPlaying |
Boolean | β | Whether vinyl is rotating |
paginatedTracks |
Array | β | Subset of tracks for current page |
playlistPage |
Number | β | Current page number (0-indexed) |
totalPages |
Number | β | Total number of pages |
showPlaylist |
Boolean | β | Show/hide playlist drawer |
isDesktop |
Boolean | β | Desktop layout (true) or mobile (false) |
onPlayPause |
Function | β | Callback for play/pause button |
onNext |
Function | β | Callback for next button |
onPrev |
Function | β | Callback for prev button |
onSeekBack |
Function | β | Callback for seek -5s button |
onSeekForward |
Function | β | Callback for seek +5s button |
onSelectTrack |
Function | β | Callback for track selection |
onTogglePlaylist |
Function | β | Callback for playlist toggle |
onPrevPage |
Function | β | Callback for prev page button |
onNextPage |
Function | β | Callback for next page button |
onUpload |
Function | β | Callback for upload button |
import { parseSpotifyCSV } from 'play-vinyl-csv';
// Input: CSV text from Spotify Exportify
const csvText = `Track Name,Artist Name(s),Album,Track Preview URL
"Song 1","Artist 1","Album 1","https://..."
"Song 2","Artist 2","Album 2","https://..."`;
// Output: Array of track objects
const tracks = parseSpotifyCSV(csvText);
// [
// {
// trackName: "Song 1",
// artistNames: "Artist 1",
// album: "Album 1",
// uri: "...",
// previewUrl: "https://...",
// index: 0
// },
// ...
// ]Your CSV file should have these columns:
Track Name,Artist Name(s),Album,Track Preview URL
Everlong,Foo Fighters,There Is Nothing Left to Lose,https://p.scdn.co/...
The Pretender,Foo Fighters,Echoes Silence Patience & Grace,https://p.scdn.co/...Note: You can export playlists from Spotify using the Exportify tool.
play-vinyl-csv/
βββ src/
β βββ App.jsx # Main app + empty state + background
β βββ PlayVinylCSV.jsx # Vinyl player + controller + playlist drawer
β βββ SpotifyParser.js # CSV parsing utility
β βββ index.css # Global styles + animations
β βββ main.jsx # React entry point
βββ tailwind.config.js # Tailwind configuration
βββ vite.config.js # Vite configuration
βββ index.html # HTML template
App.jsx (State Management)
βββ Empty State (Upload Portal)
β βββ Glowing button + file input
βββ Loaded State (Vinyl Player)
β βββ PlayVinylCSV.jsx
β βββ Vinyl Turntable
β β βββ SVG Grooves
β β βββ Center Label
β β βββ Needle Arm
β βββ Track Info
β βββ Controller Pill
β β βββ 5 Buttons
β βββ Playlist Drawer
β βββ Paginated Tracks
βββ Background (Animated Gradient Orbs)
App.jsx manages:
tracks: Array of parsed track objectscurrentTrackIndex: Current playing positionisPlaying: Playback stateshowPlaylist: Playlist drawer visibilityplaylistPage: Current page in playlistisDesktop: Responsive layout flagisLoading: CSV parsing state
PlayVinylCSV.jsx receives props for all controls and displays responsively.
@keyframes breathe {
0%, 100% {
box-shadow: 0 0 20px rgba(180, 77, 255, 0.6),
0 0 40px rgba(236, 72, 153, 0.4);
}
50% {
box-shadow: 0 0 40px rgba(180, 77, 255, 1),
0 0 80px rgba(236, 72, 153, 0.7);
}
}- Duration: 2.5 seconds
- Applied to: Upload portal, play/pause button, vinyl glow ring
- Effect: Creates pulsing neon glow that cycles between bright and dim
@keyframes vinyl-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}- Duration: 3 seconds (linear, infinite)
- Applied to: Vinyl record, center label, groove texture
- Condition: Only rotates when
isPlaying === true
Upload Portal (Empty State):
animate={{
boxShadow: [
'0 0 60px rgba(180,77,255,0.6), 0 0 100px rgba(236,72,153,0.4)',
'0 0 100px rgba(180,77,255,1), 0 0 150px rgba(236,72,153,0.8)',
'0 0 60px rgba(180,77,255,0.6), 0 0 100px rgba(236,72,153,0.4)',
]
}}
transition={{ duration: 2.5, repeat: Infinity }}Play Button (Breathing Scale):
animate={isPlaying ? { scale: [1, 1.05, 1] } : {}}
transition={isPlaying ? { duration: 1.5, repeat: Infinity } : {}}Playlist Drawer (Slide-In):
initial={{ y: isMobileDevice ? '100%' : 0, x: isMobileDevice ? 0 : '100%' }}
animate={{ y: 0, x: 0 }}
exit={{ y: isMobileDevice ? '100%' : 0, x: isMobileDevice ? 0 : '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}Background Orbs (Infinite Motion):
animate={{ y: [-100, 100], opacity: [0.2, 0.5, 0.2] }}
transition={{ duration: 10, repeat: Infinity, ease: 'easeInOut' }}| Color | Value | Usage |
|---|---|---|
| Neon Purple | #b44dff |
Primary glow, borders, buttons |
| Neon Pink | #ec4899 |
Secondary glow, gradients |
| Neon Cyan | #00ffff |
Vinyl border, grid lines |
| Dark Background | #0a0a1a |
Base dark color |
| Slate-950 | #030712 |
Gradient base |
| Slate-900 | #111827 |
Vinyl surface |
| Package | Version | Purpose |
|---|---|---|
| React | 19.2.6 | UI framework |
| Tailwind CSS | 3.4.1 | Utility-first styling |
| Framer Motion | 12.38.0 | Advanced animations |
| Lucide React | 1.14.0 | Icon library |
| Vite | 8.0.12 | Build tool & dev server |
-
Desktop:
β₯768px(md breakpoint)- Centered vinyl player
- Floating controller pill
- Side-sliding playlist drawer
- Full animations
-
Mobile:
<768px- Fixed top navbar with mini controls
- Bottom-sliding playlist drawer
- Compact layout optimized for touch
// tailwind.config.js
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})- HTML5 Audio Element: Native playback control
- Preview URLs: Spotify preview URLs (30-second clips)
- Auto-Advance: Automatically plays next track on end
- Seek Control: Β±5 second seek buttons
- Error Handling: Graceful fallback for playback errors
- Pure CSS Vinyl: No image files, all CSS gradients and SVG
- Lazy Animations: Only animate when
isPlaying === true - Efficient Re-renders:
useMemofor device detection - CSS Classes: Tailwind utility classes for minimal CSS
- Gzip Size: ~106KB (minified)
SpotifyParser.js extracts:
Track Name: Song titleArtist Name(s): Artist(s) comma-separatedAlbum: Album nameTrack URI: Spotify URI for trackTrack Preview URL: 30-second preview link
Example Parsed Output:
{
trackName: "Everlong",
artistNames: "Foo Fighters",
album: "There Is Nothing Left to Lose",
uri: "spotify:track:...",
previewUrl: "https://p.scdn.co/mp3-preview/...",
index: 0
}- Ensure file has
Track Name,Artist Name(s), and column headers - Check that preview URLs are valid Spotify URLs
- Verify browser supports HTML5 Audio element
- Check browser console for CORS errors
- Ensure Spotify preview URLs haven't expired
- Disable hardware acceleration (if on low-end device)
- Check for browser extensions interfering with rendering
- Try a different browser
- Ensure viewport meta tag is present
- Clear browser cache
- Check that window resize event is firing
MIT License - feel free to use this project for personal or commercial purposes!
- Spotify Exportify: CSV export tool
- Framer Motion: Animation library
- Tailwind CSS: Styling framework
- Lucide React: Icon set