Skip to content
edmi-ui

Audio Player

Audio player on media-chrome: play, seek, time, mute and volume, styled as an Edmi card.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Built on button; the ranges are media-chrome ranges themed through its CSS variables.

Terminal window
npx shadcn@latest add @edmi-ui/ai-audio-player
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-audio-player
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-audio-player
Terminal window
bunx shadcn@latest add @edmi-ui/ai-audio-player

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-audio-player
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-audio-player
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-audio-player
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-audio-player

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-audio-player.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
AudioPlayer,
AudioPlayerControlBar,
AudioPlayerDurationDisplay,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
AudioPlayerTimeRange,
AudioPlayerVolumeRange,
} from "@/components/ai/audio-player";
<div className="flex w-full max-w-xl flex-col gap-4">
<AudioPlayer>
<AudioPlayerControlBar>
<AudioPlayerSeekBackwardButton seekOffset={10} />
<AudioPlayerPlayButton />
<AudioPlayerSeekForwardButton seekOffset={10} />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
</AudioPlayer>
<AudioPlayer className="max-w-sm">
<AudioPlayerControlBar>
<AudioPlayerPlayButton />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
</AudioPlayerControlBar>
</AudioPlayer>
</div>
<script setup lang="ts">
import {
AudioPlayer,
AudioPlayerControlBar,
AudioPlayerDurationDisplay,
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
AudioPlayerTimeRange,
AudioPlayerVolumeRange,
} from "@/components/ai/audio-player";
</script>
<template>
<div class="flex w-full max-w-xl flex-col gap-4">
<AudioPlayer>
<AudioPlayerElement />
<AudioPlayerControlBar>
<AudioPlayerSeekBackwardButton :seek-offset="10" />
<AudioPlayerPlayButton />
<AudioPlayerSeekForwardButton :seek-offset="10" />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
</AudioPlayer>
<AudioPlayer class="max-w-sm">
<AudioPlayerElement />
<AudioPlayerControlBar>
<AudioPlayerPlayButton />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
</AudioPlayerControlBar>
</AudioPlayer>
</div>
</template>
<script lang="ts">
import {
AudioPlayer,
AudioPlayerControlBar,
AudioPlayerDurationDisplay,
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
AudioPlayerTimeRange,
AudioPlayerVolumeRange,
} from "$lib/components/ai/audio-player";
</script>
<div class="flex w-full max-w-xl flex-col gap-4">
<AudioPlayer>
{#if src}<AudioPlayerElement />{/if}
<AudioPlayerControlBar>
<AudioPlayerSeekBackwardButton seekOffset={10} />
<AudioPlayerPlayButton />
<AudioPlayerSeekForwardButton seekOffset={10} />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
</AudioPlayer>
<AudioPlayer class="max-w-sm">
{#if src}<AudioPlayerElement />{/if}
<AudioPlayerControlBar>
<AudioPlayerPlayButton />
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
</AudioPlayerControlBar>
</AudioPlayer>
</div>
registry/components/ai/audio-player.tsx
"use client";
import type { Experimental_SpeechResult as SpeechResult } from "ai";
import { cn } from "cn";
import {
MediaControlBar,
MediaController,
MediaDurationDisplay,
MediaMuteButton,
MediaPlayButton,
MediaSeekBackwardButton,
MediaSeekForwardButton,
MediaTimeDisplay,
MediaTimeRange,
MediaVolumeRange,
} from "media-chrome/react";
import type { ComponentProps, CSSProperties } from "react";
import { buttonVariants } from "@/registry/edmi/ui/button";
// Built on ui/button (DESIGN §5b): round primary play button, ghost icon buttons for seek and mute, mono
// tabular times. The ranges are media-chrome ranges themed through its CSS variables (brand fill, --muted track).
export type AudioPlayerProps = Omit<
ComponentProps<typeof MediaController>,
"audio"
>;
/** Card shell (`--card`, 1px border) around the media-chrome controller; the controls are children. */
export const AudioPlayer = ({
children,
className,
style,
...props
}: AudioPlayerProps) => (
<MediaController
audio
data-slot="ai-audio-player"
className={cn(
"block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground",
className,
)}
style={
{
"--media-background-color": "transparent",
"--media-button-icon-height": "0.875rem",
"--media-button-icon-width": "0.875rem",
"--media-control-background": "transparent",
"--media-control-hover-background": "transparent",
"--media-control-padding": "0",
"--media-font": "var(--font-sans)",
"--media-font-size": "12px",
"--media-icon-color": "currentColor",
"--media-preview-time-background": "var(--popover)",
"--media-preview-time-border-radius": "6px",
"--media-preview-time-text-shadow": "none",
"--media-primary-color": "var(--foreground)",
"--media-range-bar-color": "var(--brand)",
"--media-range-track-background": "var(--border)",
"--media-range-track-border-radius": "999px",
"--media-range-track-height": "4px",
"--media-range-thumb-background": "var(--brand)",
"--media-range-thumb-border-radius": "999px",
"--media-range-thumb-height": "12px",
"--media-range-thumb-width": "12px",
"--media-secondary-color": "var(--muted)",
"--media-text-color": "var(--foreground)",
"--media-tooltip-arrow-display": "none",
"--media-tooltip-background": "var(--popover)",
"--media-tooltip-border-radius": "6px",
...style,
} as CSSProperties
}
{...props}
>
{children}
</MediaController>
);
export type AudioPlayerElementProps = Omit<ComponentProps<"audio">, "src"> &
(
| {
data: SpeechResult["audio"];
}
| {
src: string;
}
);
export const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => (
// Captions are provided by the consumer.
<audio
data-slot="ai-audio-player-element"
slot="media"
src={
"src" in props
? props.src
: `data:${props.data.mediaType};base64,${props.data.base64}`
}
{...props}
/>
);
export type AudioPlayerControlBarProps = ComponentProps<typeof MediaControlBar>;
export const AudioPlayerControlBar = ({
className,
...props
}: AudioPlayerControlBarProps) => (
<MediaControlBar
data-slot="ai-audio-player-control-bar"
className={cn("flex w-full items-center gap-2", className)}
{...props}
/>
);
export type AudioPlayerPlayButtonProps = ComponentProps<typeof MediaPlayButton>;
/** The only filled control: a 36px round primary button. */
export const AudioPlayerPlayButton = ({
className,
...props
}: AudioPlayerPlayButtonProps) => (
<MediaPlayButton
data-slot="ai-audio-player-play-button"
className={cn(
buttonVariants({ variant: "default", size: "icon" }),
"rounded-full",
className,
)}
{...props}
/>
);
export type AudioPlayerSeekBackwardButtonProps = ComponentProps<
typeof MediaSeekBackwardButton
>;
export const AudioPlayerSeekBackwardButton = ({
seekOffset = 10,
className,
...props
}: AudioPlayerSeekBackwardButtonProps) => (
<MediaSeekBackwardButton
data-slot="ai-audio-player-seek-backward-button"
className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }),
className,
)}
seekOffset={seekOffset}
{...props}
/>
);
export type AudioPlayerSeekForwardButtonProps = ComponentProps<
typeof MediaSeekForwardButton
>;
export const AudioPlayerSeekForwardButton = ({
seekOffset = 10,
className,
...props
}: AudioPlayerSeekForwardButtonProps) => (
<MediaSeekForwardButton
data-slot="ai-audio-player-seek-forward-button"
className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }),
className,
)}
seekOffset={seekOffset}
{...props}
/>
);
export type AudioPlayerTimeDisplayProps = ComponentProps<
typeof MediaTimeDisplay
>;
export const AudioPlayerTimeDisplay = ({
className,
...props
}: AudioPlayerTimeDisplayProps) => (
<MediaTimeDisplay
data-slot="ai-audio-player-time-display"
className={cn(
"font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]",
className,
)}
{...props}
/>
);
export type AudioPlayerTimeRangeProps = ComponentProps<typeof MediaTimeRange>;
export const AudioPlayerTimeRange = ({
className,
...props
}: AudioPlayerTimeRangeProps) => (
<MediaTimeRange
data-slot="ai-audio-player-time-range"
className={cn("h-7 min-w-0 flex-1 px-1", className)}
{...props}
/>
);
export type AudioPlayerDurationDisplayProps = ComponentProps<
typeof MediaDurationDisplay
>;
export const AudioPlayerDurationDisplay = ({
className,
...props
}: AudioPlayerDurationDisplayProps) => (
<MediaDurationDisplay
data-slot="ai-audio-player-duration-display"
className={cn(
"font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]",
className,
)}
{...props}
/>
);
export type AudioPlayerMuteButtonProps = ComponentProps<typeof MediaMuteButton>;
export const AudioPlayerMuteButton = ({
className,
...props
}: AudioPlayerMuteButtonProps) => (
<MediaMuteButton
data-slot="ai-audio-player-mute-button"
className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }),
className,
)}
{...props}
/>
);
export type AudioPlayerVolumeRangeProps = ComponentProps<
typeof MediaVolumeRange
>;
export const AudioPlayerVolumeRange = ({
className,
...props
}: AudioPlayerVolumeRangeProps) => (
<MediaVolumeRange
data-slot="ai-audio-player-volume-range"
className={cn("h-7 w-[70px] px-1", className)}
{...props}
/>
);
registry/components/ai/audio-player/AudioPlayer.vue
<script setup lang="ts">
import type { HTMLAttributes, StyleValue } from "vue"
import "media-chrome"
import { cn } from "@/registry/edmi/lib/utils"
// Card shell (`--card`, 1px border) around the media-chrome controller; the controls are the default slot.
// Built on ui/button for the buttons; media-chrome ranges are themed through its CSS variables.
const props = defineProps<{
class?: HTMLAttributes["class"]
style?: StyleValue
}>()
const mediaStyle = {
"--media-background-color": "transparent",
"--media-button-icon-height": "0.875rem",
"--media-button-icon-width": "0.875rem",
"--media-control-background": "transparent",
"--media-control-hover-background": "transparent",
"--media-control-padding": "0",
"--media-font": "var(--font-sans)",
"--media-font-size": "12px",
"--media-icon-color": "currentColor",
"--media-preview-time-background": "var(--popover)",
"--media-preview-time-border-radius": "6px",
"--media-preview-time-text-shadow": "none",
"--media-primary-color": "var(--foreground)",
"--media-range-bar-color": "var(--brand)",
"--media-range-track-background": "var(--border)",
"--media-range-track-border-radius": "999px",
"--media-range-track-height": "4px",
"--media-range-thumb-background": "var(--brand)",
"--media-range-thumb-border-radius": "999px",
"--media-range-thumb-height": "12px",
"--media-range-thumb-width": "12px",
"--media-secondary-color": "var(--muted)",
"--media-text-color": "var(--foreground)",
"--media-tooltip-arrow-display": "none",
"--media-tooltip-background": "var(--popover)",
"--media-tooltip-border-radius": "6px",
}
</script>
<template>
<component
:is="'media-controller'"
audio
data-slot="ai-audio-player"
:class="cn('block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground', props.class)"
:style="[mediaStyle, props.style]"
>
<slot />
</component>
</template>
registry/components/ai/audio-player/AudioPlayerControlBar.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<component
:is="'media-control-bar'"
data-slot="ai-audio-player-control-bar"
:class="cn('flex w-full items-center gap-2', props.class)"
>
<slot />
</component>
</template>
registry/components/ai/audio-player/AudioPlayerDurationDisplay.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<component
:is="'media-duration-display'"
data-slot="ai-audio-player-duration-display"
:class="cn('font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]', props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerElement.vue
<script setup lang="ts">
import type { Experimental_SpeechResult as SpeechResult } from "ai"
import { computed } from "vue"
defineOptions({ inheritAttrs: false })
const props = defineProps<{
src?: string
data?: SpeechResult["audio"]
}>()
const audioSrc = computed(() => {
if (props.src)
return props.src
if (props.data)
return `data:${props.data.mediaType};base64,${props.data.base64}`
return undefined
})
</script>
<template>
<!-- Captions are provided by the consumer. -->
<audio
v-bind="{ ...$attrs, slot: 'media' }"
data-slot="ai-audio-player-element"
:src="audioSrc"
/>
</template>
registry/components/ai/audio-player/AudioPlayerMuteButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] }>(), {})
</script>
<template>
<component
:is="'media-mute-button'"
data-slot="ai-audio-player-mute-button"
:class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerPlayButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] }>(), {})
</script>
<template>
<component
:is="'media-play-button'"
data-slot="ai-audio-player-play-button"
:class="cn(buttonVariants({ variant: 'default', size: 'icon' }), 'rounded-full', props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerSeekBackwardButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"], seekOffset?: number }>(), {seekOffset: 10})
</script>
<template>
<component
:is="'media-seek-backward-button'"
data-slot="ai-audio-player-seek-backward-button"
:seekoffset="props.seekOffset"
:class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerSeekForwardButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"], seekOffset?: number }>(), {seekOffset: 10})
</script>
<template>
<component
:is="'media-seek-forward-button'"
data-slot="ai-audio-player-seek-forward-button"
:seekoffset="props.seekOffset"
:class="cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerTimeDisplay.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<component
:is="'media-time-display'"
data-slot="ai-audio-player-time-display"
:class="cn('font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]', props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerTimeRange.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<component
:is="'media-time-range'"
data-slot="ai-audio-player-time-range"
:class="cn('h-7 min-w-0 flex-1 px-1', props.class)"
/>
</template>
registry/components/ai/audio-player/AudioPlayerVolumeRange.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<component
:is="'media-volume-range'"
data-slot="ai-audio-player-volume-range"
:class="cn('h-7 w-[70px] px-1', props.class)"
/>
</template>
registry/components/ai/audio-player/index.ts
export { default as AudioPlayer } from "./AudioPlayer.vue";
export { default as AudioPlayerControlBar } from "./AudioPlayerControlBar.vue";
export { default as AudioPlayerDurationDisplay } from "./AudioPlayerDurationDisplay.vue";
export { default as AudioPlayerElement } from "./AudioPlayerElement.vue";
export { default as AudioPlayerMuteButton } from "./AudioPlayerMuteButton.vue";
export { default as AudioPlayerPlayButton } from "./AudioPlayerPlayButton.vue";
export { default as AudioPlayerSeekBackwardButton } from "./AudioPlayerSeekBackwardButton.vue";
export { default as AudioPlayerSeekForwardButton } from "./AudioPlayerSeekForwardButton.vue";
export { default as AudioPlayerTimeDisplay } from "./AudioPlayerTimeDisplay.vue";
export { default as AudioPlayerTimeRange } from "./AudioPlayerTimeRange.vue";
export { default as AudioPlayerVolumeRange } from "./AudioPlayerVolumeRange.vue";
src/lib/registry/ai/audio-player/audio-player-control-bar.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<media-control-bar
data-slot="ai-audio-player-control-bar"
class={cn("flex w-full items-center gap-2", className)}
{...restProps}
>
{@render children?.()}
</media-control-bar>
src/lib/registry/ai/audio-player/audio-player-duration-display.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<media-duration-display
data-slot="ai-audio-player-duration-display"
class={cn("font-mono text-xs text-muted-foreground tabular-nums [--media-font:var(--font-mono)] [--media-text-color:var(--muted-foreground)]", className)}
{...restProps}
>
{@render children?.()}
</media-duration-display>
src/lib/registry/ai/audio-player/audio-player-element.svelte
<script lang="ts">
import type { Experimental_SpeechResult as SpeechResult } from "ai";
import type { HTMLAudioAttributes } from "svelte/elements";
// Pass `src`, or `data` (an AI SDK speech result). Captions are provided by the consumer.
let {
src,
data,
...restProps
}: Omit<HTMLAudioAttributes, "src"> & {
src?: string;
data?: SpeechResult["audio"];
} = $props();
const audioSrc = $derived(
src ?? (data ? `data:${data.mediaType};base64,${data.base64}` : undefined)
);
</script>
<!-- svelte-ignore a11y_media_has_caption -->
<audio
data-slot="ai-audio-player-element"
src={audioSrc}
{...restProps}
{...{ slot: "media" }}
></audio>
src/lib/registry/ai/audio-player/audio-player-mute-button.svelte
<script lang="ts">
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLElement> = $props();
</script>
<media-mute-button
data-slot="ai-audio-player-mute-button"
class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)}
{...restProps}
>
{@render children?.()}
</media-mute-button>
src/lib/registry/ai/audio-player/audio-player-play-button.svelte
<script lang="ts">
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
children,
...restProps
}: HTMLAttributes<HTMLElement> = $props();
</script>
<media-play-button
data-slot="ai-audio-player-play-button"
class={cn(buttonVariants({ variant: "default", size: "icon" }), "rounded-full", className)}
{...restProps}
>
{@render children?.()}
</media-play-button>
src/lib/registry/ai/audio-player/audio-player-seek-backward-button.svelte
<script lang="ts">
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
seekOffset = 10,
children,
...restProps
}: HTMLAttributes<HTMLElement> & { seekOffset?: number } = $props();
</script>
<media-seek-backward-button
data-slot="ai-audio-player-seek-backward-button"
seekoffset={seekOffset}
class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)}
{...restProps}
>
{@render children?.()}
</media-seek-backward-button>
src/lib/registry/ai/audio-player/audio-player-seek-forward-button.svelte
<script lang="ts">
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
seekOffset = 10,
children,
...restProps
}: HTMLAttributes<HTMLElement> & { seekOffset?: number } = $props();
</script>
<media-seek-forward-button
data-slot="ai-audio-player-seek-forward-button"
seekoffset={seekOffset}
class={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), className)}
{...restProps}
>
{@render children?.()}
</media-seek-forward-button>
src/lib/registry/ai/audio-player/audio-player-time-display.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<media-time-display
data-slot="ai-audio-player-time-display"
class={cn("font-mono text-xs text-foreground tabular-nums [--media-font:var(--font-mono)]", className)}
{...restProps}
>
{@render children?.()}
</media-time-display>
src/lib/registry/ai/audio-player/audio-player-time-range.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<media-time-range
data-slot="ai-audio-player-time-range"
class={cn("h-7 min-w-0 flex-1 px-1", className)}
{...restProps}
>
{@render children?.()}
</media-time-range>
src/lib/registry/ai/audio-player/audio-player-volume-range.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLElement> = $props();
</script>
<media-volume-range
data-slot="ai-audio-player-volume-range"
class={cn("h-7 w-[70px] px-1", className)}
{...restProps}
>
{@render children?.()}
</media-volume-range>
src/lib/registry/ai/audio-player/audio-player.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { onMount } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
// Card shell (`--card`, 1px border) around the media-chrome controller; the controls are children.
// Built on ui/button for the buttons; media-chrome ranges are themed through its CSS variables.
let {
class: className,
style,
children,
...restProps
}: HTMLAttributes<HTMLElement> = $props();
// Registers the <media-*> custom elements in the browser only (SSR-safe).
onMount(() => {
import("media-chrome");
});
const mediaStyle =
"--media-background-color: transparent; --media-button-icon-height: 0.875rem; --media-button-icon-width: 0.875rem; --media-control-background: transparent; --media-control-hover-background: transparent; --media-control-padding: 0; --media-font: var(--font-sans); --media-font-size: 12px; --media-icon-color: currentColor; --media-preview-time-background: var(--popover); --media-preview-time-border-radius: 6px; --media-preview-time-text-shadow: none; --media-primary-color: var(--foreground); --media-range-bar-color: var(--brand); --media-range-track-background: var(--border); --media-range-track-border-radius: 999px; --media-range-track-height: 4px; --media-range-thumb-background: var(--brand); --media-range-thumb-border-radius: 999px; --media-range-thumb-height: 12px; --media-range-thumb-width: 12px; --media-secondary-color: var(--muted); --media-text-color: var(--foreground); --media-tooltip-arrow-display: none; --media-tooltip-background: var(--popover); --media-tooltip-border-radius: 6px;";
</script>
<media-controller
audio
data-slot="ai-audio-player"
class={cn(
"block w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-foreground",
className
)}
style="{mediaStyle} {style ?? ''}"
{...restProps}
>
{@render children?.()}
</media-controller>
src/lib/registry/ai/audio-player/index.ts
import Root from "./audio-player.svelte";
import ControlBar from "./audio-player-control-bar.svelte";
import DurationDisplay from "./audio-player-duration-display.svelte";
import Element from "./audio-player-element.svelte";
import MuteButton from "./audio-player-mute-button.svelte";
import PlayButton from "./audio-player-play-button.svelte";
import SeekBackwardButton from "./audio-player-seek-backward-button.svelte";
import SeekForwardButton from "./audio-player-seek-forward-button.svelte";
import TimeDisplay from "./audio-player-time-display.svelte";
import TimeRange from "./audio-player-time-range.svelte";
import VolumeRange from "./audio-player-volume-range.svelte";
export {
ControlBar,
ControlBar as AudioPlayerControlBar,
DurationDisplay,
DurationDisplay as AudioPlayerDurationDisplay,
Element,
Element as AudioPlayerElement,
MuteButton,
MuteButton as AudioPlayerMuteButton,
PlayButton,
PlayButton as AudioPlayerPlayButton,
Root,
//
Root as AudioPlayer,
SeekBackwardButton,
SeekBackwardButton as AudioPlayerSeekBackwardButton,
SeekForwardButton,
SeekForwardButton as AudioPlayerSeekForwardButton,
TimeDisplay,
TimeDisplay as AudioPlayerTimeDisplay,
TimeRange,
TimeRange as AudioPlayerTimeRange,
VolumeRange,
VolumeRange as AudioPlayerVolumeRange,
};
Part Prop Type Default
AudioPlayer className, style merged into the media-chrome controller (--media-* variables can be overridden in style)
AudioPlayerElement src or data string, or an AI SDK speech audio result
AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton seekOffset number (seconds) 10
other parts media-chrome element props
  • The player is a --card surface with a 1px border; play is a 36px round primary button, seek and mute are ghost icon buttons.
  • Times are mono and tabular; the time range fills with --brand over a --muted track.
  • Deviation from the board: the board draws a waveform for the time range; the real component uses the media-chrome range (a waveform needs decoded audio).
  • Vue and Svelte use the <media-*> custom elements directly (seek-offset / seekOffset prop on the seek buttons). In Svelte the root registers media-chrome on mount.