Media Widget
Player widgets — a circular radial control pad and a now-playing card with a scrollable playlist.
Preview
@props([
// Centre button starts paused; the inline Alpine toggle swaps the icon.
'playing' => false,
])
{{--
Each wedge is a real, keyboard-operable control: a <x-ui.button> laid over
the wedge via <foreignObject>. Clicking/activating one sets `active` to its
index, which toggles `aria-pressed` on that button and tints its decorative
wedge — the widget has no backing audio element in this block, so "active"
tracks the last control the visitor engaged rather than a live volume/seek
state.
--}}
@php
// Donut control pad geometry. viewBox 0 0 192 192, centre (96,96),
// outer radius 88, inner radius 40. Four 90deg arc segments with a small
// angular gap so the wedges read as discrete controls.
$cx = 96;
$cy = 96;
$outer = 88;
$inner = 40;
$gap = 6; // degrees of padding between segments
// Each segment: [start angle, end angle] in degrees, plus its label + icon key.
// 0deg points right; angles increase clockwise in SVG screen space (y-down).
$segments = [
['from' => 225, 'to' => 315, 'label' => __('Volume up'), 'icon' => 'volume-up', 'tx' => 96, 'ty' => 26],
['from' => 45, 'to' => 135, 'label' => __('Volume down'), 'icon' => 'volume-down', 'tx' => 96, 'ty' => 166],
['from' => 135, 'to' => 225, 'label' => __('Skip back'), 'icon' => 'skip-back', 'tx' => 26, 'ty' => 96],
['from' => 315, 'to' => 405, 'label' => __('Skip forward'), 'icon' => 'skip-forward', 'tx' => 166, 'ty' => 96],
];
// Build a donut-wedge path between two radii across an angle range.
$point = function ($r, $deg) use ($cx, $cy) {
$rad = deg2rad($deg);
return [
round($cx + $r * cos($rad), 3),
round($cy + $r * sin($rad), 3),
];
};
$wedge = function ($from, $to) use ($point, $outer, $inner, $gap) {
$a0 = $from + $gap / 2;
$a1 = $to - $gap / 2;
$large = ($a1 - $a0) > 180 ? 1 : 0;
[$ox0, $oy0] = $point($outer, $a0);
[$ox1, $oy1] = $point($outer, $a1);
[$ix1, $iy1] = $point($inner, $a1);
[$ix0, $iy0] = $point($inner, $a0);
return "M {$ox0} {$oy0} "
."A {$outer} {$outer} 0 {$large} 1 {$ox1} {$oy1} "
."L {$ix1} {$iy1} "
."A {$inner} {$inner} 0 {$large} 0 {$ix0} {$iy0} "
."Z";
};
$iconPaths = [
'volume-up' => '<path d="M3 9v6h4l5 5V4L7 9H3z"/><path d="M16 8a5 5 0 0 1 0 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
'volume-down' => '<path d="M3 9v6h4l5 5V4L7 9H3z"/>',
'skip-back' => '<path d="M18 6 9 12l9 6V6zM6 6h2v12H6z"/>',
'skip-forward' => '<path d="M6 6l9 6-9 6V6zM16 6h2v12h-2z"/>',
];
@endphp
<x-ui.card
x-data="{ playing: {{ Js::from((bool) $playing) }}, active: null }"
{{ $attributes->merge(['class' => 'w-full max-w-[15rem] aspect-square flex items-center justify-center p-4']) }}
>
<div data-slot="media-widget-radial" class="relative w-full h-full">
<svg
viewBox="0 0 192 192"
class="w-full h-full"
role="group"
aria-label="{{ __('Playback controls') }}"
>
@foreach ($segments as $i => $seg)
<g class="group/seg">
<path
d="{{ $wedge($seg['from'], $seg['to']) }}"
class="fill-secondary transition-colors group-hover/seg:fill-muted"
:class="{ 'fill-muted': active === {{ $i }} }"
/>
{{-- The real control: a button positioned over the wedge via foreignObject. --}}
<foreignObject x="{{ $seg['tx'] - 22 }}" y="{{ $seg['ty'] - 22 }}" width="44" height="44">
<x-ui.button
type="button"
variant="ghost"
shape="pill"
:icon-label="$seg['label']"
x-on:click="active = {{ $i }}"
x-bind:aria-pressed="active === {{ $i }} ? 'true' : 'false'"
class="!size-full text-muted-foreground"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
class="size-5 {{ in_array($seg['icon'], ['skip-back', 'skip-forward'], true) ? 'rtl:-scale-x-100' : '' }}"
>
{!! $iconPaths[$seg['icon']] !!}
</svg>
</x-ui.button>
</foreignObject>
</g>
@endforeach
</svg>
{{-- Centre play / pause toggle. --}}
<button
type="button"
x-on:click="playing = !playing"
x-bind:aria-label="playing ? @js(__('Pause')) : @js(__('Play'))"
aria-label="{{ __('Play') }}"
title="{{ __('Play or pause') }}"
class="absolute inset-0 m-auto inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
style="width: 41.6%; height: 41.6%"
>
<span class="sr-only">{{ __('Play or pause') }}</span>
<svg x-show="!playing" class="size-2/5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
<svg x-show="playing" x-cloak class="size-2/5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg>
</button>
</div>
</x-ui.card>
Installation
php artisan ui:add blocks/media-widget-radial
Registry contract
Install confidence
php artisan ui:add blocks/media-widget-radial
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.5
- License
- open
- Stability
- stable
- Contract
- v1
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/media-widget-radial.blade.php |
Registry dependencies
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.
# Brok UI block: Media Widget Radial (`media-widget-radial`)
A circular donut control pad with four arc segments (volume up/down, skip back/forward) around a centre play/pause toggle.
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/media-widget-radial
```
## Render it
```blade
<x-blocks.media-widget-radial />
```
## Props
- `playing` (bool, default `false`) — Initial play/pause state; the centre button toggles it.
## Use when
- Presenting playback controls as a circular donut pad with four wedge segments, volume up/down and skip back/forward, around a centre play/pause button.
- Wanting a compact, decorative radial control layout rather than a linear transport bar.
## Avoid when
- Needing a working player with real seek/volume state or a track list; use music-player-card or media-widget-playlist.
## Rules
- Render the installed block with `<x-blocks.media-widget-radial />` 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/media-widget-radial
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/media-widget-radial.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 playback controls as a circular donut pad with four wedge segments, volume up/down and skip back/forward, around a centre play/pause button.
- Wanting a compact, decorative radial control layout rather than a linear transport bar.
Avoid when
- Needing a working player with real seek/volume state or a track list; use music-player-card or media-widget-playlist.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
Usage
Render the block as a component, passing data where useful:
<x-blocks.media-widget-radial />
Built from primitives
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 file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
// Centre button starts paused; the inline Alpine toggle swaps the icon.
'playing' => false,
])
{{--
Each wedge is a real, keyboard-operable control: a <x-ui.button> laid over
the wedge via <foreignObject>. Clicking/activating one sets `active` to its
index, which toggles `aria-pressed` on that button and tints its decorative
wedge — the widget has no backing audio element in this block, so "active"
tracks the last control the visitor engaged rather than a live volume/seek
state.
--}}
@php
// Donut control pad geometry. viewBox 0 0 192 192, centre (96,96),
// outer radius 88, inner radius 40. Four 90deg arc segments with a small
// angular gap so the wedges read as discrete controls.
$cx = 96;
$cy = 96;
$outer = 88;
$inner = 40;
$gap = 6; // degrees of padding between segments
// Each segment: [start angle, end angle] in degrees, plus its label + icon key.
// 0deg points right; angles increase clockwise in SVG screen space (y-down).
$segments = [
['from' => 225, 'to' => 315, 'label' => __('Volume up'), 'icon' => 'volume-up', 'tx' => 96, 'ty' => 26],
['from' => 45, 'to' => 135, 'label' => __('Volume down'), 'icon' => 'volume-down', 'tx' => 96, 'ty' => 166],
['from' => 135, 'to' => 225, 'label' => __('Skip back'), 'icon' => 'skip-back', 'tx' => 26, 'ty' => 96],
['from' => 315, 'to' => 405, 'label' => __('Skip forward'), 'icon' => 'skip-forward', 'tx' => 166, 'ty' => 96],
];
// Build a donut-wedge path between two radii across an angle range.
$point = function ($r, $deg) use ($cx, $cy) {
$rad = deg2rad($deg);
return [
round($cx + $r * cos($rad), 3),
round($cy + $r * sin($rad), 3),
];
};
$wedge = function ($from, $to) use ($point, $outer, $inner, $gap) {
$a0 = $from + $gap / 2;
$a1 = $to - $gap / 2;
$large = ($a1 - $a0) > 180 ? 1 : 0;
[$ox0, $oy0] = $point($outer, $a0);
[$ox1, $oy1] = $point($outer, $a1);
[$ix1, $iy1] = $point($inner, $a1);
[$ix0, $iy0] = $point($inner, $a0);
return "M {$ox0} {$oy0} "
."A {$outer} {$outer} 0 {$large} 1 {$ox1} {$oy1} "
."L {$ix1} {$iy1} "
."A {$inner} {$inner} 0 {$large} 0 {$ix0} {$iy0} "
."Z";
};
$iconPaths = [
'volume-up' => '<path d="M3 9v6h4l5 5V4L7 9H3z"/><path d="M16 8a5 5 0 0 1 0 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
'volume-down' => '<path d="M3 9v6h4l5 5V4L7 9H3z"/>',
'skip-back' => '<path d="M18 6 9 12l9 6V6zM6 6h2v12H6z"/>',
'skip-forward' => '<path d="M6 6l9 6-9 6V6zM16 6h2v12h-2z"/>',
];
@endphp
<x-ui.card
x-data="{ playing: {{ Js::from((bool) $playing) }}, active: null }"
{{ $attributes->merge(['class' => 'w-full max-w-[15rem] aspect-square flex items-center justify-center p-4']) }}
>
<div data-slot="media-widget-radial" class="relative w-full h-full">
<svg
viewBox="0 0 192 192"
class="w-full h-full"
role="group"
aria-label="{{ __('Playback controls') }}"
>
@foreach ($segments as $i => $seg)
<g class="group/seg">
<path
d="{{ $wedge($seg['from'], $seg['to']) }}"
class="fill-secondary transition-colors group-hover/seg:fill-muted"
:class="{ 'fill-muted': active === {{ $i }} }"
/>
{{-- The real control: a button positioned over the wedge via foreignObject. --}}
<foreignObject x="{{ $seg['tx'] - 22 }}" y="{{ $seg['ty'] - 22 }}" width="44" height="44">
<x-ui.button
type="button"
variant="ghost"
shape="pill"
:icon-label="$seg['label']"
x-on:click="active = {{ $i }}"
x-bind:aria-pressed="active === {{ $i }} ? 'true' : 'false'"
class="!size-full text-muted-foreground"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
class="size-5 {{ in_array($seg['icon'], ['skip-back', 'skip-forward'], true) ? 'rtl:-scale-x-100' : '' }}"
>
{!! $iconPaths[$seg['icon']] !!}
</svg>
</x-ui.button>
</foreignObject>
</g>
@endforeach
</svg>
{{-- Centre play / pause toggle. --}}
<button
type="button"
x-on:click="playing = !playing"
x-bind:aria-label="playing ? @js(__('Pause')) : @js(__('Play'))"
aria-label="{{ __('Play') }}"
title="{{ __('Play or pause') }}"
class="absolute inset-0 m-auto inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card"
style="width: 41.6%; height: 41.6%"
>
<span class="sr-only">{{ __('Play or pause') }}</span>
<svg x-show="!playing" class="size-2/5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
<svg x-show="playing" x-cloak class="size-2/5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg>
</button>
</div>
</x-ui.card>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.5 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v1 - Foundation
-
≥ 1.0.0