diff --git a/docs/guide/getting-started/supported-languages.md b/docs/guide/getting-started/supported-languages.md
index a9d28247..55ea6b2f 100644
--- a/docs/guide/getting-started/supported-languages.md
+++ b/docs/guide/getting-started/supported-languages.md
@@ -24,6 +24,8 @@ Examples:
- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/js/notifications)
![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+- [Media Player](https://github.com/Aylur/astal/tree/main/examples/js/media-player)
+![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
## Lua
diff --git a/examples/js/media-player/.gitignore b/examples/js/media-player/.gitignore
new file mode 100644
index 00000000..6850183b
--- /dev/null
+++ b/examples/js/media-player/.gitignore
@@ -0,0 +1,2 @@
+@girs/
+node_modules/
\ No newline at end of file
diff --git a/examples/js/media-player/README.md b/examples/js/media-player/README.md
new file mode 100644
index 00000000..4e3d237b
--- /dev/null
+++ b/examples/js/media-player/README.md
@@ -0,0 +1,5 @@
+# Media Player
+
+![mpris](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
+Using [Mpris](https://aylur.github.io/astal/guide/libraries/mpris).
diff --git a/examples/js/media-player/app.ts b/examples/js/media-player/app.ts
new file mode 100644
index 00000000..5b7558a0
--- /dev/null
+++ b/examples/js/media-player/app.ts
@@ -0,0 +1,11 @@
+import { App, Widget } from "astal/gtk3"
+import style from "./style.scss"
+import MprisPlayers from "./widget/MediaPlayer"
+
+App.start({
+ instanceName: "players",
+ css: style,
+ main: () => {
+ new Widget.Window({}, MprisPlayers())
+ }
+})
diff --git a/examples/js/media-player/style.scss b/examples/js/media-player/style.scss
new file mode 100644
index 00000000..2e2f625c
--- /dev/null
+++ b/examples/js/media-player/style.scss
@@ -0,0 +1 @@
+@use "./widget/MediaPlayer.scss";
diff --git a/examples/js/media-player/widget/MediaPlayer.scss b/examples/js/media-player/widget/MediaPlayer.scss
new file mode 100644
index 00000000..e1597c28
--- /dev/null
+++ b/examples/js/media-player/widget/MediaPlayer.scss
@@ -0,0 +1,56 @@
+// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
+$fg-color: #{"@theme_fg_color"};
+$bg-color: #{"@theme_bg_color"};
+
+window {
+ all: unset;
+}
+
+box.MediaPlayer {
+ padding: .6rem;
+ background-color: $bg-color;
+
+ box.cover-art {
+ min-width: 120px;
+ min-height: 120px;
+ border-radius: 9px;
+ margin-right: .6rem;
+ background-size: contain;
+ background-position: center;
+ }
+
+ box.title {
+ label {
+ font-weight: bold;
+ font-size: 1.1em;
+ }
+ }
+
+ scale {
+ padding: 0;
+ margin: .4rem 0;
+
+ trough {
+ min-height: 8px;
+ }
+
+ highlight {
+ background-color: $fg-color;
+ }
+
+ slider {
+ all: unset;
+ }
+ }
+
+ centerbox.actions {
+ min-width: 220px;
+
+ button {
+ min-width: 0;
+ min-height: 0;
+ padding: .4rem;
+ margin: 0 .2rem;
+ }
+ }
+}
diff --git a/examples/js/media-player/widget/MediaPlayer.tsx b/examples/js/media-player/widget/MediaPlayer.tsx
new file mode 100644
index 00000000..06c7e77f
--- /dev/null
+++ b/examples/js/media-player/widget/MediaPlayer.tsx
@@ -0,0 +1,94 @@
+import { Astal, Gtk } from "astal/gtk3"
+import Mpris from "gi://AstalMpris"
+import { bind } from "astal"
+
+function lengthStr(length: number) {
+ const min = Math.floor(length / 60)
+ const sec = Math.floor(length % 60)
+ const sec0 = sec < 10 ? "0" : ""
+ return `${min}:${sec0}${sec}`
+}
+
+
+function MediaPlayer({ player }: { player: Mpris.Player }) {
+ const { START, END } = Gtk.Align
+
+ const title = bind(player, "title").as(t =>
+ t || "Unknown Track")
+
+ const artist = bind(player, "artist").as(a =>
+ a || "Unknown Artist")
+
+ const coverArt = bind(player, "coverArt").as(c =>
+ `background-image: url('${c}')`)
+
+ const playerIcon = bind(player, "entry").as(e =>
+ Astal.Icon.lookup_icon(e) ? e : "audio-x-generic-symbolic")
+
+ const position = bind(player, "position").as(p => player.length > 0
+ ? p / player.length : 0)
+
+ const playIcon = bind(player, "playbackStatus").as(s =>
+ s === Mpris.PlaybackStatus.PLAYING
+ ? "media-playback-pause-symbolic"
+ : "media-playback-start-symbolic"
+ )
+
+ return
+
+
+
+
+
+
+
+ l > 0)}
+ onDragged={({ value }) => player.position = value * player.length}
+ value={position}
+ />
+
+
+
+
+}
+
+export default function MprisPlayers() {
+ const mpris = Mpris.get_default()
+ return
+ {bind(mpris, "players").as(arr => arr.map(player => (
+
+ )))}
+
+}