Svelte Audio UI
DocsParticles
0
Overview
  • Introduction
  • Get Started
Components
  • Audio Player
  • Audio Provider
  • Audio Queue
  • Audio Track
  • Audio Playback Speed
UI
  • Fader
  • Knob
  • Slider
  • Sortable List
  • XY Pad
Libs
  • Audio Store
  • HTML Audio
Resources
  • llms.txt
  • llms-full.txt

On This Page

InstallationUsageAudioTrackAudioTrackListGrid LayoutSortableSortable GridAPI ReferenceAudioTrackPropsAudioTrackListPropsExamplesStore mode (main queue)Controlled mode (external list)FilteringWith removalRelated

Audio Track

Previous Next

Track components for displaying and managing audio tracks, with store and controlled modes.


Track components for displaying and managing audio tracks. These components support two modes:
  • Store mode: reads from and controls the global audio queue provided by audioStore.
  • Controlled mode: accepts an explicit list or single track for use in search results or external lists.

Installation

pnpm dlx shadcn-svelte@latest add https://svelte-audio-ui.vercel.app/r/track.json
npx shadcn-svelte@latest add https://svelte-audio-ui.vercel.app/r/track.json
npx shadcn-svelte@latest add https://svelte-audio-ui.vercel.app/r/track.json
bun x shadcn-svelte@latest add https://svelte-audio-ui.vercel.app/r/track.json

Install @lucide/svelte :

pnpm i @lucide/svelte
npm i @lucide/svelte
yarn install @lucide/svelte
bun install @lucide/svelte

Copy and paste the following code into your project.

<script lang="ts">
  import { ListMusic } from "@lucide/svelte";
  import { audioStore } from "$lib/audio-store.svelte.js";
  import type { Track } from "$lib/html-audio.js";
  import { cn } from "$lib/utils.js";
  import AudioTrack from "./audio-track.svelte";
  import { SortableList } from "$lib/components/ui/audio/elements/sortable-list/index.js";

  type Variant = "default" | "grid";

  interface Props {
    /** When omitted, reads from audioStore.queue (store mode). */
    tracks?: Track[];
    onTrackSelect?: (index: number, track?: Track) => void;
    onTrackRemove?: (trackId: string) => void;
    sortable?: boolean;
    showCover?: boolean;
    variant?: Variant;
    emptyLabel?: string;
    emptyDescription?: string;
    filterQuery?: string;
    filterFn?: (track: Track) => boolean;
    class?: string;
  }

  let {
    tracks: externalTracks,
    onTrackSelect,
    onTrackRemove,
    sortable = false,
    showCover = true,
    variant = "default",
    emptyLabel = "No tracks found",
    emptyDescription = "Try adding some tracks",
    filterQuery = "",
    filterFn,
    class: className = "",
  }: Props = $props();

  const isExternalTracks = $derived(!!externalTracks);

  // ── Filtered tracks ─────────────────────────────────────────────────────────
  const displayTracks = $derived.by<Track[]>(() => {
    let base: Track[] = externalTracks ?? audioStore.queue;

    if (filterFn) {
      base = base.filter(filterFn);
    } else if (filterQuery.trim()) {
      const q = filterQuery.toLowerCase();
      base = base.filter(
        (t) => t.title?.toLowerCase().includes(q) || t.artist?.toLowerCase().includes(q)
      );
    }
    return base;
  });

  const isFiltered = $derived(filterQuery.trim().length > 0 || !!filterFn);

  // ── Variant classes ─────────────────────────────────────────────────────────
  const listClass = $derived(
    variant === "grid" ? "grid grid-cols-1 gap-2 xl:grid-cols-2" : "space-y-0.5"
  );

  // ── Track click handler ─────────────────────────────────────────────────────
  function handleTrackClick(track: Track, index: number) {
    if (isExternalTracks) {
      onTrackSelect?.(index, track);
      return;
    }
    // Store mode: find real queue index (filter may shift indices)
    const queueIndex = audioStore.queue.findIndex((t) => t.id === track.id);
    if (queueIndex >= 0) {
      if (audioStore.currentTrack?.id === track.id) {
        audioStore.togglePlay();
      } else {
        audioStore.setQueueAndPlay(audioStore.queue, queueIndex);
      }
      onTrackSelect?.(queueIndex, audioStore.queue[queueIndex]);
    } else {
      onTrackSelect?.(index, track);
    }
  }

  // ── Reorder handler (sortable, store mode only) ────────────────────────────
  function handleReorder(reordered: { id: string | number }[]) {
    if (isFiltered || isExternalTracks) return;

    const reorderedTracks = reordered
      .map((r) => displayTracks.find((t: Track) => String(t.id) === String(r.id)))
      .filter((t): t is Track => t !== undefined);

    const newCurrentIndex =
      audioStore.currentTrack?.id !== undefined
        ? reorderedTracks.findIndex((t) => t.id === audioStore.currentTrack!.id)
        : -1;

    const finalIndex =
      newCurrentIndex >= 0
        ? newCurrentIndex
        : Math.max(0, Math.min(audioStore.currentQueueIndex, reorderedTracks.length - 1));

    audioStore.setQueue(reorderedTracks, finalIndex);
  }
</script>

{#if displayTracks.length === 0}
  <!-- Empty state ──────────────────────────────────────────────────────────── -->
  <div
    class={cn(
      "mx-auto flex size-full flex-col items-center justify-center gap-3",
      "bg-muted/30 rounded-lg border p-8 text-center",
      className
    )}
  >
    <div class="bg-muted flex size-10 items-center justify-center rounded-full">
      <ListMusic class="text-muted-foreground size-5" />
    </div>
    <div class="space-y-1">
      <p class="text-sm font-medium">{emptyLabel}</p>
      <p class="text-muted-foreground text-xs/relaxed">{emptyDescription}</p>
    </div>
  </div>
{:else if sortable && !isFiltered}
  <!-- Sortable list ─────────────────────────────────────────────────────────── -->
  <div class={cn("no-scrollbar w-full overflow-y-auto", className)}>
    <SortableList
      items={displayTracks
        .filter((t: Track) => t.id !== undefined)
        .map((t: Track) => ({ id: String(t.id), _track: t }))}
      onDrop={handleReorder}
      class={variant === "grid" ? "grid grid-cols-1 gap-2 xl:grid-cols-2" : "gap-0.5"}
    >
      {#snippet item(row)}
        {@const track = (row as { _track: Track })._track}
        {@const idx = displayTracks.findIndex((t: Track) => t.id === track.id)}
        <AudioTrack
          {track}
          index={idx >= 0 ? idx : undefined}
          {showCover}
          showDragHandle={true}
          showRemove={!!onTrackRemove}
          onRemove={onTrackRemove}
          onclick={() => handleTrackClick(track, idx >= 0 ? idx : 0)}
        />
      {/snippet}
    </SortableList>
  </div>
{:else}
  <!-- Plain list ────────────────────────────────────────────────────────────── -->
  <div class={cn("no-scrollbar w-full overflow-y-auto", className)}>
    <div class={cn("w-full", listClass)}>
      {#each displayTracks as track, idx (track.id)}
        <AudioTrack
          {track}
          index={idx}
          {showCover}
          showDragHandle={false}
          showRemove={!!onTrackRemove}
          onRemove={onTrackRemove}
          onclick={() => handleTrackClick(track, idx)}
        />
      {/each}
    </div>
  </div>
{/if}

Usage

Import the components:

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
</script>

AudioTrack

Displays a single track with optional cover, metadata, and playback controls. Use either:

  • trackId to render an item from the global audio queue (store mode), or
  • track to render a provided track object (controlled mode).

Loading...

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
  import { audioStore } from "$lib/audio-store.svelte.js";
</script>
 
{#if audioStore.queue[0]}
  <AudioTrack.Root trackId={audioStore.queue[0].id} />
{:else}
  <p class="text-muted-foreground text-sm">No tracks in queue.</p>
{/if}
<!-- From queue by id -->
<AudioTrack.Root trackId={track.id} />
 
<!-- With explicit data -->
<AudioTrack.Root track={customTrack} onclick={() => handlePlay(customTrack)} />

AudioTrackList

Renders a list of tracks. Pass tracks for a controlled list, otherwise the component renders the global queue.

Key features:

  • Text filtering via filterQuery or a custom filterFn.
  • Optional drag-and-drop (sortable) — reordering updates the queue only when not filtered and when tracks is not provided.
  • Per-item remove action and play/pause controls (configurable via props).

Loading...

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
</script>
 
<AudioTrack.List
  onTrackSelect={(index) => console.log("selected queue index", index)}
/>
<!-- Store mode: renders global queue -->
<AudioTrack.List onTrackSelect={(index) => console.log(index)} />
 
<!-- Controlled mode: render custom set -->
<AudioTrack.List tracks={searchResults} onTrackSelect={handleSelect} />

Grid Layout

Loading...

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
</script>
 
<AudioTrack.List variant="grid" />

Sortable

Loading...

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
</script>
 
<AudioTrack.List
  sortable
  onTrackSelect={(index) => console.log("selected queue index", index)}
/>

Sortable Grid

Loading...

<script lang="ts">
  import * as AudioTrack from "$lib/components/ui/audio/track/index.js";
</script>
 
<AudioTrack.List sortable variant="grid" />

API Reference

AudioTrack

Props

Prop Type Default Description
trackId string | number - Load a track from the global queue (store mode). Do not use together with track.
track Track - Render a provided track object (controlled mode). Do not use together with trackId.
index number - Display index (shown as one-based when showCover is false).
onclick () => void - Click handler invoked when the track row is clicked.
onRemove (trackId: string) => void - Called when the remove button is used. Receives the track id as a string.
showRemove boolean false Whether to show the remove (✕) button. Always hidden for the currently playing track.
showPlayPause boolean true Show play/pause control button.
showDragHandle boolean false Show a drag handle (use together with a sortable list).
showCover boolean true Show album artwork. Falls back to a music icon when no artwork is available.
class string - Additional CSS classes on the track row element.

Note: Use either trackId (store mode) or track (controlled mode). Both should not be used together. Store mode requires AudioProvider to be set up. Cover images are taken from track.artwork or track.images[0]. Live tracks show a Live badge and the duration is hidden — live detection uses htmlAudio.isLive().


AudioTrackList

Props

Prop Type Default Description
tracks Track[] - Controlled list of tracks. When omitted, the component reads from the global queue (store mode).
onTrackSelect (index: number, track?: Track) => void - Called when a track is selected or played. In store mode the index is the queue index; in controlled mode it refers to the provided tracks array.
onTrackRemove (trackId: string) => void - Remove handler used by the per-item remove button.
sortable boolean false Enable drag-and-drop reordering. Reordering updates the store queue only when not filtered and when tracks is not provided.
showCover boolean true Show track cover image.
variant "default" | "grid" "default" Layout variant: stacked list or responsive grid.
filterQuery string - Simple text filter (matches title or artist, case-insensitive).
filterFn (track: Track) => boolean - Custom filter function. When provided, filterQuery is ignored.
emptyLabel string "No tracks found" Label shown when the list is empty.
emptyDescription string "Try adding some tracks" Description shown in the empty state.
class string - Additional CSS classes.

Store vs Controlled Mode: When tracks is omitted, the component reads from the global queue. When tracks is provided, it operates in controlled mode using the provided array.

Sortable Behavior: When sortable is enabled, reordering will only update the store queue when the list is not filtered (filterQuery and filterFn are not used) AND the component is in store mode (tracks prop is not provided). Keep sortable disabled while filtering to avoid unexpected reordering behavior.

Examples

Store mode (main queue)

<AudioTrack.List
  sortable
  onTrackSelect={(i) => console.log("play queue index", i)}
/>

Controlled mode (external list)

<AudioTrack.List
  tracks={searchResults}
  variant="grid"
  onTrackSelect={(index, track) => addToQueue(track)}
/>

Filtering

<!-- Simple text filter -->
<AudioTrack.List filterQuery="jazz" />
 
<!-- Custom filter function -->
<AudioTrack.List filterFn={(t) => t.genre === "jazz"} />

With removal

<AudioTrack.List onTrackRemove={(id) => removeFromQueue(id)} />

Related

  • Audio Player — player controls to use alongside track components
  • Audio Provider — required wrapper that manages queue state
  • Audio Queue — queue dialog and ordering controls
Audio Queue Audio Playback Speed
Built by ddtamn. The source code is available on Github