Skip to content
Brok UI

Loading…

No results

Music Player

Open source

Audio players — a spinning vinyl record, a transport-control card, horizontal and vertical Spotify-style cards, and an album crate.

Preview

1 of 5

resources/views/blocks/music-player-vinyl.blade.php Blade
@props([
    'src' => null,
    'cover' => null,
    'title' => 'Midnight Drive',
    'artist' => 'The Synthwaves',
    'autoplay' => false,
])

<div
    x-data="uiVinylPlayer({ src: {{ $src ? \Illuminate\Support\Js::from($src) : 'null' }}, autoplay: {{ $autoplay ? 'true' : 'false' }} })"
    data-slot="music-player-01"
    {{ $attributes->merge(['class' => 'mx-auto flex w-full max-w-md flex-col items-center gap-6 text-foreground sm:flex-row sm:items-center']) }}
>
    {{-- Turntable: the record is the interactive element. --}}
    <div class="relative mx-auto aspect-square w-56 shrink-0 sm:w-64">
        {{-- The vinyl record (a real button so it is keyboard + screen-reader friendly). --}}
        <button
            type="button"
            @click="toggle()"
            :aria-pressed="playing"
            :aria-label="playing ? '{{ __('Pause') }}' : '{{ __('Play') }}'"
            aria-label="{{ __('Play') }}"
            class="ui-vinyl-disc group relative block aspect-square w-full overflow-hidden rounded-full border border-border shadow-lg outline-none ring-offset-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring motion-safe:animate-[ui-vinyl-spin_4s_linear_infinite] [animation-play-state:paused]"
            :class="{ '[animation-play-state:running]': playing, '[animation-play-state:paused]': !playing }"
        >
            {{-- Blurred cover fill behind the grooves. --}}
            @if ($cover)
                <img src="{{ $cover }}" alt="" aria-hidden="true" class="absolute inset-0 size-full scale-110 object-cover opacity-40 blur-xl" />
            @else
                <span aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-primary/50 via-accent/40 to-secondary/50 blur-xl"></span>
            @endif

            {{-- Concentric vinyl grooves. --}}
            <span aria-hidden="true" class="ui-vinyl-grooves absolute inset-0 rounded-full"></span>

            {{-- Center label: cover art (or token gradient) + center hole. --}}
            <span aria-hidden="true" class="absolute inset-0 m-auto flex aspect-square w-2/5 items-center justify-center overflow-hidden rounded-full border border-border/60 shadow-inner">
                @if ($cover)
                    <img src="{{ $cover }}" alt="" class="size-full object-cover" />
                @else
                    <span class="size-full bg-gradient-to-br from-primary via-accent to-secondary"></span>
                @endif
                {{-- Center hole. --}}
                <span class="absolute size-3 rounded-full bg-background shadow-inner"></span>
            </span>

            {{-- Play/pause glyph overlay (decorative; the button label carries the meaning). --}}
            <span aria-hidden="true" class="absolute inset-0 m-auto flex aspect-square w-2/5 items-center justify-center rounded-full bg-background/0 text-foreground opacity-0 transition-opacity group-hover:bg-background/30 group-hover:opacity-100">
                <svg class="size-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" x-show="!playing">
                    <path d="M8 5.14v13.72a1 1 0 0 0 1.54.84l10.29-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" />
                </svg>
                <svg class="size-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" x-show="playing" x-cloak>
                    <path d="M7 4a1 1 0 0 1 1 1v14a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v14a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1Z" />
                </svg>
            </span>
        </button>

        {{-- Tonearm: swings onto the record while playing, lifts off when paused. --}}
        <div
            aria-hidden="true"
            class="absolute -end-2 -top-2 origin-top-right rtl:origin-top-left transition-transform duration-700 ease-out motion-reduce:transition-none"
            :class="playing ? 'rotate-[26deg]' : 'rotate-0'"
        >
            {{-- Pivot mount. --}}
            <span class="absolute end-0 top-0 size-6 rounded-full border border-border bg-card shadow-sm"></span>
            {{-- Arm. --}}
            <span class="absolute end-3 top-3 block h-28 w-2 -translate-y-1 rotate-[18deg] rounded-full bg-muted-foreground/70 shadow-sm"></span>
            {{-- Headshell / needle. --}}
            <span class="absolute end-[2.1rem] top-[6.5rem] block h-4 w-3 rotate-[18deg] rounded-sm bg-foreground shadow-sm"></span>
        </div>
    </div>

    {{-- Track metadata. --}}
    <div class="min-w-0 text-center sm:text-start">
        <p class="truncate text-lg font-semibold">{{ $title }}</p>
        <p class="truncate text-sm text-muted-foreground">{{ $artist }}</p>
        <x-ui.badge variant="secondary" class="mt-2" x-text="playing ? '{{ __('Now playing') }}' : '{{ __('Paused') }}'">{{ __('Paused') }}</x-ui.badge>
    </div>

    @if ($src)
        <audio x-ref="audio" preload="none" src="{{ $src }}" class="hidden"></audio>
    @endif
</div>
resources/js/ui/music-player-vinyl.js JS
/**
 * Vinyl record music player behavior.
 *
 * A spinning vinyl record that toggles play/pause when clicked. State is a
 * single `playing` flag that drives every visual cue (the CSS spin animation's
 * play-state, the tonearm swing, the play/pause label) and, when a real audio
 * source is wired, the actual <audio> element.
 *
 * If `src` is given the element referenced by `x-ref="audio"` is the source of
 * truth: its native `play`/`pause`/`ended` events keep `playing` in sync, so
 * the record stops spinning and the arm lifts when playback ends. With no
 * `src` it is a silent visual demo that still spins and toggles.
 *
 * Self-registers on `alpine:init` so import order does not matter. Audio is
 * paused in `destroy()`.
 *
 * Named `uiVinylPlayer` to avoid clashing with the album-art `uiMusicPlayer`
 * card transport.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiVinylPlayer', (config = {}) => ({
        hasAudio: !!config.src,
        autoplay: config.autoplay ?? false,
        playing: false,

        init() {
            const audio = this.$refs.audio;
            if (audio) {
                // Mirror the native media state so external controls or the
                // track ending keep the visuals honest.
                audio.addEventListener('play', () => { this.playing = true; });
                audio.addEventListener('pause', () => { this.playing = false; });
                audio.addEventListener('ended', () => { this.playing = false; });
            }
            if (this.autoplay) this.play();
        },

        destroy() {
            const audio = this.$refs.audio;
            if (audio) audio.pause();
            this.playing = false;
        },

        play() {
            const audio = this.$refs.audio;
            if (audio) {
                // play() resolves the `play` event which flips `playing`; if the
                // browser blocks autoplay, fall back to the visual-only state.
                const p = audio.play();
                if (p && typeof p.catch === 'function') p.catch(() => { this.playing = true; });
            } else {
                this.playing = true;
            }
        },

        pause() {
            const audio = this.$refs.audio;
            if (audio) audio.pause();
            else this.playing = false;
        },

        toggle() {
            if (this.playing) this.pause();
            else this.play();
        },
    }));
});

Installation

terminal
php artisan ui:add blocks/music-player-vinyl

Installs these dependencies automatically:

Registry contract

Install confidence

php artisan ui:add blocks/music-player-vinyl writes only the generated targets below. The CLI validates each file hash before writing and prompts before replacing local changes unless --force is used.

Version
1.0.4
License
open
Stability
stable
Contract
v1
Foundation
≥ 1.0.0
Type Generated target
blade resources/views/blocks/music-player-vinyl.blade.php
js resources/js/ui/music-player-vinyl.js

Registry dependencies

badge

Package dependencies

composer: jml/brok:^0.2

npm: alpinejs

Use with AI

A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.

music-player-vinyl.md
# Brok UI block: Music Player (`music-player-vinyl`)

A spinning vinyl record music player: the album art is the record's center label and a blurred background fill, a stylized tonearm swings onto the record while playing, and the record spins. Click the record to toggle play/pause; wires a real <audio> element when a source is given.

Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.

## Install

```bash
php artisan ui:add blocks/music-player-vinyl
```

## Render it

```blade
<x-blocks.music-player-vinyl />
```

## Props

- `src` (mixed|null, default `null`) — Audio source URL; when set, wires a real <audio> element that the disc button controls.
- `cover` (mixed|null, default `null`) — Album art URL used as the blurred backdrop and the disc's centre label; falls back to a token gradient.
- `title` (string, default `Midnight Drive`) — Track title.
- `artist` (string, default `The Synthwaves`) — Track artist.
- `autoplay` (bool, default `false`) — Starts playback automatically when a src is given.

## Use when

- Presenting a single track as a spinning vinyl record with a tonearm that swings onto the disc while playing.
- Wanting the simplest music player, driven only by a click-to-toggle play/pause control.

## Avoid when

- Needing a seek bar, transport controls, or volume; use music-player-card, music-player-spotify, or music-player-spotify-vertical.

## Rules

- Render the installed block with `<x-blocks.music-player-vinyl />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.

## Links

- Docs: https://brokui.dev/blocks/music-player-vinyl
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/music-player-vinyl.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Guidance

Use when

  • Presenting a single track as a spinning vinyl record with a tonearm that swings onto the disc while playing.
  • Wanting the simplest music player, driven only by a click-to-toggle play/pause control.

Avoid when

  • Needing a seek bar, transport controls, or volume; use music-player-card, music-player-spotify, or music-player-spotify-vertical.

Anti-patterns

  • Do not replace semantic props with conflicting utility classes.
  • Do not remove labels, focus styles, or state attributes.
Anatomy
vinyl disc button with cover label tonearm track title and artist now playing badge optional audio element

Usage

Render the block as a component, passing data where useful:

resources/views/welcome.blade.php Blade
<x-blocks.music-player-vinyl />

Built from primitives

Composes <brok:*> Semantic tokens Re-themeable RTL-ready

This block composes installed <brok:*> primitives and semantic design tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by editing CSS variables; flip the preview to RTL to confirm it mirrors.

Source

The exact, editable files ui:add writes into your app. The preview above renders this same source — there are no preview-only blocks.

resources/views/blocks/music-player-vinyl.blade.php Blade
@props([
    'src' => null,
    'cover' => null,
    'title' => 'Midnight Drive',
    'artist' => 'The Synthwaves',
    'autoplay' => false,
])

<div
    x-data="uiVinylPlayer({ src: {{ $src ? \Illuminate\Support\Js::from($src) : 'null' }}, autoplay: {{ $autoplay ? 'true' : 'false' }} })"
    data-slot="music-player-01"
    {{ $attributes->merge(['class' => 'mx-auto flex w-full max-w-md flex-col items-center gap-6 text-foreground sm:flex-row sm:items-center']) }}
>
    {{-- Turntable: the record is the interactive element. --}}
    <div class="relative mx-auto aspect-square w-56 shrink-0 sm:w-64">
        {{-- The vinyl record (a real button so it is keyboard + screen-reader friendly). --}}
        <button
            type="button"
            @click="toggle()"
            :aria-pressed="playing"
            :aria-label="playing ? '{{ __('Pause') }}' : '{{ __('Play') }}'"
            aria-label="{{ __('Play') }}"
            class="ui-vinyl-disc group relative block aspect-square w-full overflow-hidden rounded-full border border-border shadow-lg outline-none ring-offset-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring motion-safe:animate-[ui-vinyl-spin_4s_linear_infinite] [animation-play-state:paused]"
            :class="{ '[animation-play-state:running]': playing, '[animation-play-state:paused]': !playing }"
        >
            {{-- Blurred cover fill behind the grooves. --}}
            @if ($cover)
                <img src="{{ $cover }}" alt="" aria-hidden="true" class="absolute inset-0 size-full scale-110 object-cover opacity-40 blur-xl" />
            @else
                <span aria-hidden="true" class="absolute inset-0 bg-gradient-to-br from-primary/50 via-accent/40 to-secondary/50 blur-xl"></span>
            @endif

            {{-- Concentric vinyl grooves. --}}
            <span aria-hidden="true" class="ui-vinyl-grooves absolute inset-0 rounded-full"></span>

            {{-- Center label: cover art (or token gradient) + center hole. --}}
            <span aria-hidden="true" class="absolute inset-0 m-auto flex aspect-square w-2/5 items-center justify-center overflow-hidden rounded-full border border-border/60 shadow-inner">
                @if ($cover)
                    <img src="{{ $cover }}" alt="" class="size-full object-cover" />
                @else
                    <span class="size-full bg-gradient-to-br from-primary via-accent to-secondary"></span>
                @endif
                {{-- Center hole. --}}
                <span class="absolute size-3 rounded-full bg-background shadow-inner"></span>
            </span>

            {{-- Play/pause glyph overlay (decorative; the button label carries the meaning). --}}
            <span aria-hidden="true" class="absolute inset-0 m-auto flex aspect-square w-2/5 items-center justify-center rounded-full bg-background/0 text-foreground opacity-0 transition-opacity group-hover:bg-background/30 group-hover:opacity-100">
                <svg class="size-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" x-show="!playing">
                    <path d="M8 5.14v13.72a1 1 0 0 0 1.54.84l10.29-6.86a1 1 0 0 0 0-1.68L9.54 4.3A1 1 0 0 0 8 5.14Z" />
                </svg>
                <svg class="size-6 drop-shadow" viewBox="0 0 24 24" fill="currentColor" x-show="playing" x-cloak>
                    <path d="M7 4a1 1 0 0 1 1 1v14a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1Zm10 0a1 1 0 0 1 1 1v14a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1Z" />
                </svg>
            </span>
        </button>

        {{-- Tonearm: swings onto the record while playing, lifts off when paused. --}}
        <div
            aria-hidden="true"
            class="absolute -end-2 -top-2 origin-top-right rtl:origin-top-left transition-transform duration-700 ease-out motion-reduce:transition-none"
            :class="playing ? 'rotate-[26deg]' : 'rotate-0'"
        >
            {{-- Pivot mount. --}}
            <span class="absolute end-0 top-0 size-6 rounded-full border border-border bg-card shadow-sm"></span>
            {{-- Arm. --}}
            <span class="absolute end-3 top-3 block h-28 w-2 -translate-y-1 rotate-[18deg] rounded-full bg-muted-foreground/70 shadow-sm"></span>
            {{-- Headshell / needle. --}}
            <span class="absolute end-[2.1rem] top-[6.5rem] block h-4 w-3 rotate-[18deg] rounded-sm bg-foreground shadow-sm"></span>
        </div>
    </div>

    {{-- Track metadata. --}}
    <div class="min-w-0 text-center sm:text-start">
        <p class="truncate text-lg font-semibold">{{ $title }}</p>
        <p class="truncate text-sm text-muted-foreground">{{ $artist }}</p>
        <x-ui.badge variant="secondary" class="mt-2" x-text="playing ? '{{ __('Now playing') }}' : '{{ __('Paused') }}'">{{ __('Paused') }}</x-ui.badge>
    </div>

    @if ($src)
        <audio x-ref="audio" preload="none" src="{{ $src }}" class="hidden"></audio>
    @endif
</div>
resources/js/ui/music-player-vinyl.js JS
/**
 * Vinyl record music player behavior.
 *
 * A spinning vinyl record that toggles play/pause when clicked. State is a
 * single `playing` flag that drives every visual cue (the CSS spin animation's
 * play-state, the tonearm swing, the play/pause label) and, when a real audio
 * source is wired, the actual <audio> element.
 *
 * If `src` is given the element referenced by `x-ref="audio"` is the source of
 * truth: its native `play`/`pause`/`ended` events keep `playing` in sync, so
 * the record stops spinning and the arm lifts when playback ends. With no
 * `src` it is a silent visual demo that still spins and toggles.
 *
 * Self-registers on `alpine:init` so import order does not matter. Audio is
 * paused in `destroy()`.
 *
 * Named `uiVinylPlayer` to avoid clashing with the album-art `uiMusicPlayer`
 * card transport.
 */
document.addEventListener('alpine:init', () => {
    window.Alpine.data('uiVinylPlayer', (config = {}) => ({
        hasAudio: !!config.src,
        autoplay: config.autoplay ?? false,
        playing: false,

        init() {
            const audio = this.$refs.audio;
            if (audio) {
                // Mirror the native media state so external controls or the
                // track ending keep the visuals honest.
                audio.addEventListener('play', () => { this.playing = true; });
                audio.addEventListener('pause', () => { this.playing = false; });
                audio.addEventListener('ended', () => { this.playing = false; });
            }
            if (this.autoplay) this.play();
        },

        destroy() {
            const audio = this.$refs.audio;
            if (audio) audio.pause();
            this.playing = false;
        },

        play() {
            const audio = this.$refs.audio;
            if (audio) {
                // play() resolves the `play` event which flips `playing`; if the
                // browser blocks autoplay, fall back to the visual-only state.
                const p = audio.play();
                if (p && typeof p.catch === 'function') p.catch(() => { this.playing = true; });
            } else {
                this.playing = true;
            }
        },

        pause() {
            const audio = this.$refs.audio;
            if (audio) audio.pause();
            else this.playing = false;
        },

        toggle() {
            if (this.playing) this.pause();
            else this.play();
        },
    }));
});

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.4
Status
Stable
License
open
Deprecation
Not deprecated
Contract
v1
Foundation
≥ 1.0.0