Components / Artist List

Artist List

An interactive artist showcase component with hover-activated profiles. Features smooth spring animations, social links, and a responsive layout that adapts beautifully across all screen sizes.

Inspired by Vercel Ship 2025

Preview

Usage

tsx
import { useArtist } from "@/hooks/use-artist"
import { ArtistList } from "@/components/vercel-speaker/artist-list"
import { Loader } from "lucide-react"

export default function ArtistPage() {
  const { artists, loading } = useArtist()

  if (loading) {
    return (
      <div className="flex h-64 items-center justify-center">
        <Loader className="animate-spin text-4xl text-zinc-400" />
      </div>
    )
  }

  return (
    <div className="overflow-hidden border border-zinc-800">
      <ArtistList artists={artists} />
    </div>
  )
}

Features

  • Hover interactions — Artist profiles reveal on hover with smooth transitions
  • Spring animations — Bouncy, natural-feeling motion using Motion library
  • Mobile responsive — Profile image moves to top on smaller screens with tap support
  • Social links — Animated icon wobble effect on hover

Props

ComponentPropTypeDescription
ArtistListartistsArtist[]Array of artist objects
ArtistMemberartistArtistIndividual artist data
ArtistMemberisActivebooleanWhether this artist is selected
ArtistMemberonHover() => voidCallback when hovered/clicked
ArtistProfileartistArtistArtist to display profile image

Artist Type

The Artist interface defines the shape of each artist object.

artist.tstypescript
import type { StaticImageData } from 'next/image'

export interface Artist {
  id: string
  name: string
  image: string | StaticImageData
  album: string
  social: {
    twitter?: string
    instagram?: string
  }
}

Dependencies

This component requires the following packages:

bash
pnpm add motion lucide-react react-icons

Source Code

This component requires 6 files. Copy them into your components/vercel-speaker/ folder and the hook into hooks/.

tsx
'use client'
import { type Artist } from "./artist"
import { useState } from "react"
import { ArtistMember } from "./artist-member"
import { ArtistProfile } from "./artist-profile"

interface ArtistListProps {
  artists: Artist[]
}

export function ArtistList({ artists }: ArtistListProps) {
  const [activeArtist, setActiveArtist] = useState<Artist | null>(artists[0] || null)

  const handleHover = (artist: Artist) => {
    setActiveArtist(artist)
  }

  return (
    <div className="flex flex-col md:flex-row md:items-stretch md:divide-x divide-zinc-800">
      {/* Mobile: Show active artist profile at top */}
      <div className="relative aspect-[16/9] md:hidden">
        {activeArtist && <ArtistProfile artist={activeArtist} />}
      </div>

      {/* Artist list */}
      <div className="w-full md:w-3/5 min-w-0 divide-y divide-zinc-800">
        {artists.map((artist) => (
          <ArtistMember
            key={artist.id}
            artist={artist}
            isActive={activeArtist?.id === artist.id}
            onHover={() => handleHover(artist)}
          />
        ))}
      </div>

      {/* Desktop: Show active artist profile on right */}
      <div className="relative hidden md:block md:w-2/5 flex-shrink-0 min-h-[400px]">
        {activeArtist && <ArtistProfile artist={activeArtist} />}
      </div>
    </div>
  )
}