Tape Deck

A 3D shelf of cassette tapes. Hover lifts one, click pulls it out and plays a little beat.

Read how it's built

Preview

Setup

Install with the shadcn CLI

$npx shadcn@latest add https://kinetikui.vercel.app/r/tape-deck.json

One command adds every file, the npm packages and any Kinetik components it builds on. Needs a components.json; the CLI offers to create one if your project has none.

Or install manually

  1. 1

    Create a components/ui/tape-deck/ folder and copy every file from the Code tab into it. Each file does one job, so you can read or change a piece without the rest.

  2. 2

    Import and render:

    Example
    import { TapeDeck, TapeCover } from "@/components/ui/tape-deck"
    
    <TapeDeck
      tapes={[
        { id: "one", title: "Night Bus", artist: "Lowercase Lions",
          cover: <TapeCover background="#2fc0c9" text="NIGHT BUS" /> },
      ]}
    />

No packages to install: it is one file with its own styles, plain React and the Web Audio API.

Usage

Give it a list of tapes. A cover can be the built-in TapeCover, an <img>, or any JSX. Without a src, each tape plays a small generated beat at its bpm.

import { TapeDeck, TapeCover, type TapeItem } from "@/components/ui/tape-deck"

const tapes: TapeItem[] = [
  {
    id: "imgnest",
    title: "ImgNest",
    artist: "Next.js · PostgreSQL",
    cover: <TapeCover background="#2fc0c9" color="#0b2e33" text={"IMG\nNEST"} side="A" />,
  },
  {
    id: "craftui",
    title: "CraftUI",
    artist: "React component library",
    cover: <img src="/covers/craftui.png" alt="" />,
    src: "/audio/craftui.mp3",
  },
]

export function Shelf() {
  return <TapeDeck tapes={tapes} className="h-[600px]" />
}

To restyle it, override --fg, --muted, --line and --bg on the deck. To keep music playing after a tape closes, create a player with useTapePlayer outside and pass it as player.

Props

PropTypeDefaultNotes
tapesTapeItem[]—The tapes on the shelf, in order.
classNamestring—Size and theme overrides. Default height is clamp(520px, 80vh, 820px).
playerTapePlayer—Outside player. Without one the deck plays tapes itself.
defaultSelectednumber—Index of a tape to open on mount.
TapeItemTypeNotes
idstringUnique. Also varies the generated beat.
titlestringFirst line under the open tape.
artiststringSecond line.
coverReactNodeFills the label: TapeCover, an img or your own markup.
srcstring?Audio file. Without it a small beat plays.
bpmnumber?Tempo of the beat and reel speed. Defaults to 100.

Accessibility

  • Every tape is a focusable button; Enter or Space opens it
  • ← → move along the shelf, or between tapes once one is open; Space plays or pauses; Escape closes
  • Vertical page scroll is left alone: only horizontal scroll or drag moves the shelf
  • With reduced motion on, tapes jump into place and the reels stop spinning