Skip to content

Repository files navigation

🎵 Bottom-Bar Radio Player — The Audio Never Stops While Visitors Browse

Live Demo No API Key Drop--in Component

English · Português · Español · Italiano

▶ Try the live demo — click play, then navigate between the pages: the music never stops.

Description

A bottom-bar HTML5 radio player that works as a drop-in JavaScript component: two script tags inject the whole player (HTML, CSS and fonts) into any page of your site. With seamless navigation enabled (default), clicks on internal links are intercepted and only the page content is swapped — the audio keeps playing without any interruption while visitors browse your site.

🇧🇷 Player de rádio fixo no rodapé que NÃO PARA quando o visitante navega pelo site. Duas linhas de código em cada página e pronto: música tocando agora com capa, histórico, letra, clipe do YouTube sincronizado e um site demo completo (notícias, vídeos, programação, equipe) configurável sem tocar em código.

Key Features:

  • Embeddable component — no HTML to copy; the player injects itself.
  • Uninterrupted audio across pages — internal navigation swaps content without reloading (SPA-lite), so the stream never stops.
  • Playback state persistence — station, playing state and volume survive full page loads; playback auto-resumes (or resumes on the first tap when the browser blocks autoplay).
  • Audio playback with play/pause, volume and station switching, plus smooth volume fade in/out (no audio "pop").
  • Loading spinner while the stream buffers and automatic reconnection with backoff when the network drops.
  • Dynamic audio visualizer that reacts to the music in real time (off on mobile to save battery; paused when the tab is hidden).
  • Track progress bar along the bottom edge of the dock, in two flavours: a plain line, or a liquid wave riding the crest of the fill. It is clipped by the dock's own rounded shape, so it follows the corner in every visual style, and only appears when the metadata API reports the track's time.
  • Now playing metadata via the twj.es API — cover art comes straight from the API payload, with search.php + iTunes (music-only) as fallbacks.
  • Station list with thumbnails and information.
  • Song history with covers (up to 10 recently played tracks).
  • Lyrics via lyrics.ovh with LRCLIB fallback — no API key required, with request caching.
  • Dynamic accent color extracted from the current cover art.
  • Clip mode — when the metadata API sends a youtubeId, a floating mini-player shows the music video of the song on air, seeked to the same position; it survives page navigation.
  • Live TV — with tv_url in a station, a "TV" button opens the live video in a compact centered window (✕, click-outside or Esc to close).
  • One audio source at a time — starting the radio pauses any playing video (site videos, clip mode) and closes the live TV; stopping the video brings the radio back. Enforced inside play(), so every entry point (dock button, station switch, lock screen, auto-resume, RadioPlayer.play()) obeys it.
  • Marquee for long titles — song and artist names that don't fit slide instead of being cut off, and only while they overflow.
  • Integrated social sharing for Facebook, Twitter, and WhatsApp.
  • Media Session integration (lock screen / notification controls) routed through the same play/pause path as the dock button, so fade, persisted state and the video rule apply there too.
  • Mobile-first dock — on phones the song title gets the space (no cramped 24px column): cover · "on air + station" · title · artist · one big play button, extra controls in a labeled sheet, and safe-area padding for iPhones. A handle on top of the dock collapses it out of the way — on desktop too. Tapping the cover opens the station list.
  • Photo gallery with lightbox (arrows, keyboard, swipe) on the demo site.
  • "How to listen" card with the official app badges (Google Play / App Store) and the Alexa phrase.
  • Installable (PWA) — manifest, icons, offline shell via service worker and an "install app" button.
  • Five design languages — glassmorphism (default), claymorphism, minimalism, liquid glass and spatial UI, picked in the generator and applied to the site and the dock at once.
  • Four spoken languages — Portuguese, English, Spanish and Italian, switchable from the header. The whole interface follows, including the dock; switching doesn't stop the audio.

Demo Screenshots

Demo Screenshot

Demo Screenshot

How do I add the player to my website? (Installation)

  1. Download the player files:

    • Download or clone this repository and host the js/, css/ and assets/ folders (plus config.js and custom.css) on your site. css/ui-styles.css ships inside css/ and is what powers the alternative visual styles.
  2. Configure your radio stations:

    • Open the config.js file.
    • Edit the window.streams.stations variable and replace the example stations with your own.
    • For each station, fill in: name, hash, description, URLs for logo, album art, background cover, audio stream URL, social links, app links, etc.
    • Important: use absolute URLs (or paths valid from every page) for the images, since the player can be embedded at any depth of your site.
  3. Add the component to every page of your site:

    <script src="config.js"></script>
    <script src="js/radioplayer.js"></script>

    That's it — the player builds itself at the bottom of the page. See index.html and pagina2.html for a working two-page demo of the uninterrupted navigation.

  4. (Optional) Configure the site content:

    • The demo site sections (hero slides, news with full articles, YouTube videos, photo gallery, weekly schedule, team, "how to listen" card, social links, footer) all live in content.js and are rendered by site.js + site.css.
    • Edit content.js by hand, or use the visual generator: open gerador.html locally in your browser — it pre-fills the forms with your current content, lets you add/remove items, and generates a new content.js to copy or download. Replace the file at the root of the site and you're done.

⚠️ Do not publish gerador.html to your production site. It is a local admin tool — anyone with the URL could read your whole configuration and craft replacement files. Keep it on your machine (or delete it from the server after deploying).

Installing on WordPress

There's a dedicated WordPress plugin, maintained in its own repository: jailsonsb2/bottom-radioplayer-wordpress. It wraps this component with a proper wp-admin settings page (General / Stations / Appearance tabs, stations repeater with the native media picker, Clip Mode built in) — no file editing needed. It is not published on wordpress.org; the linked repo ships a ready-to-upload bottom-radioplayer.zip at its root and has the full install instructions.

Seamless Navigation (audio never stops)

Enabled by default. When a visitor clicks an internal link, the component fetches the target page, swaps the <body> content (keeping the player alive), updates the title/history and re-runs the new page's scripts. External links, target="_blank", download links and anchors are left alone.

  • To disable it, set seamless: false in config.js (window.streams.seamless = false). Navigation then reloads normally and the player auto-resumes playback on the next page (on the first tap if the browser blocks autoplay).
  • To exclude a specific link from interception, add the data-no-seamless attribute to it.
  • Links to /wp-admin/ and wp-login.php are always excluded automatically — WordPress's admin area isn't part of the site's front-end layout.
  • Pages should share the same base layout/CSS; stylesheets found in the target page's <head> are adopted automatically.

Clip Mode (music video of the current song)

If your now-playing metadata API returns a youtubeId field (or youtube_id) in the payload, a "Clipe" button automatically appears in the player (feature-detected — sites whose API doesn't send the field never see the button). With clip mode on:

  • the floating mini-player opens with the music video of the song that is playing (radio audio pauses, video audio takes over), synchronized with the radio position (start = elapsed from the API) instead of starting from zero;
  • every song change just swaps the embed to the new clip;
  • songs without a clip close the video and fall back to the radio automatically;
  • the video keeps playing across page navigation (data-seamless-keep), and the preference is remembered;
  • the radio and a video never play at the same time. Any path that starts the radio — the dock button, switching station, the lock screen, auto-resume after a reload, RadioPlayer.play() — first pauses the YouTube embeds and closes the live TV; pausing or finishing the video hands the audio back to the radio. Pausing the clip by hand also turns clip mode off, so the next song doesn't reopen the video over the audio you just chose.

The component also exposes each track to the site: window.RadioPlayer.currentTrack and the radioplayer:track DOM event (detail: { title, artist, art, cover, youtubeId }), plus radioplayer:ready when the player mounts.

The player on phones

Most listeners arrive on a phone, so the dock is laid out for that screen first (custom.css, @media (max-width: 991px)):

  • The title owns the width. Previous/next switch stations, so on phones they leave the bar (they come back on tablets, ≥768px) and the cover becomes the shortcut to the station list — a chevron badge marks it. With a single station in config.js the player gets a single-station class and those buttons disappear at every size.
  • Context line● AO VIVO · Station name above the song, so the station is still identifiable while a track plays.
  • Extra controls with labels — the "…" button opens a 3-column sheet (TV, Clip, History, Share, Lyrics, Stations). Silent icons in circles told nobody what they did; volume is left out (hardware buttons own it, and iOS ignores audio.volume).
  • Collapse handle (at every width) — the tab on top of the dock slides it off-screen so the page is fully readable; the audio keeps playing and the state survives seamless navigation. How far it has to travel comes from --dock-bottom on #app-player .player, so a visual style that lifts or flattens the dock only has to restate that variable.
  • Safe area — the dock offset uses env(safe-area-inset-bottom), clearing the iPhone home bar.
  • History and stations open as a full-width sheet above the dock instead of a narrow right-anchored panel.

Photo Gallery and "How to listen"

Two content-driven sections of the demo site, both configured in content.js (or in gerador.html):

gallery: [
    { image: "photos/studio.jpg", thumb: "photos/studio-small.jpg", caption: "Main studio" },
],
apps:   { android: "https://play.google.com/…", ios: "", alexa: "https://www.amazon.com/dp/…" },
listen: { title: "How to listen", text: "…", alexaPhrase: "Alexa, play My Radio" },
  • Gallery — a responsive grid in the #galeria section; clicking a photo opens a lightbox with arrows, keyboard (←/→/Esc), swipe and a counter. thumb is optional (use it to serve a lighter thumbnail); with an empty list the whole section hides itself.
  • How to listen — a card in the "About" section with the official store badges plus the Alexa phrase (linked to your skill when apps.alexa is filled). The same apps entries feed the store badges in the footer, and fall back to window.streams.stations[0].apps from config.js when empty.

Two more optional fields live under about in content.js:

about: {
    city: "São Paulo",                                        // weather chip + footer map
    donation: { url: "https://ko-fi.com/…", label: "Support us" },
}
  • Weather chip and mapabout.city drives both the little temperature chip in the header and the map card in the "About" section. Leave it empty and both disappear. The map sits under the history text, in the same column, so the section doesn't leave a hole beside the side cards.
  • Donation buttonabout.donation puts a highlighted button in the header. Empty url hides it.

Section order (and empty sections)

The home page sections are stacked in whatever order content.order lists them — put your station's strongest content on top. The same order is applied to the header menu, on every page:

order: ["galeria", "noticias", "videos", "programacao", "equipe", "contato"],
  • Valid names are exactly those six. The hero slides are always first and the footer always last, so they aren't listed.
  • The field is optional: drop it (or list only a couple of names) and the missing sections keep the default order at the end — an older content.js still renders everything.
  • Sections with no content hide themselves, title and menu link included. A station with no videos doesn't need to touch order — an empty videos: [] is enough to make the section and its menu entry disappear.
  • gerador.html has an "Ordem das seções" block with ↑/↓ buttons that writes this list for you.

PWA (installable app)

The demo site ships as an installable app: manifest.json, icons in assets/pwa/, the sw.js service worker and pwa.js (which registers it and shows the Install app button in the header and inside the "How to listen" card — on iOS the button explains the Share › Add to Home Screen path instead, since Safari has no install prompt).

  • Requires HTTPS (or localhost). GitHub Pages, Netlify and any host with TLS work out of the box.
  • Caching rules in sw.js: everything editable — pages, CSS, JS, JSON — is network-first, so an online visitor always gets what you just published and the cache only answers when the network fails (offline, or your server down); images, icons and fonts use stale-while-revalidate; the audio stream, metadata APIs, YouTube, maps and weather are cross-origin and never touched.
  • If a page looks frozen in an old version, check the server is actually up: with it down, the service worker legitimately serves the offline copy. During development keep DevTools › Application › Service Workers › Bypass for network checked, or hard-reload (Ctrl+Shift+R), which skips the worker entirely.
  • After deploying a new version, bump const VERSION at the top of sw.js: the old cache is dropped and anyone with the site open gets a "new version available" toast.
  • Replace assets/pwa/icon-*.png and the name/colors in manifest.json with your radio's own. To drop the feature entirely, delete the <script src="pwa.js"> line from the pages.

Advanced Customization

  • Images: Replace the images in the assets folder with your own.

  • Lyrics: the "Lyrics" button shows the current song's lyrics (lyrics.ovh with LRCLIB fallback). To turn the feature off, set lyrics: false in config.js (window.streams.lyrics = false) — the button and modal disappear and no lyrics request is ever made.

  • Dock effects (bargraph and progress bar): two decorations, both set in config.js and both on by default.

    field values
    visualizer true (default) / falsefalse doesn't merely hide the bars: the element never reaches the DOM, so the AudioContext is never opened.
    progress "wave" (default) / "simple" / false. true is still accepted as an alias for "wave", so older config files keep working.

    The progress bar only shows up when the metadata API reports now_playing.elapsed and now_playing.duration — on a pure live stream it stays invisible whatever you set. Between polls (10 s apart) it advances on a local clock anchored to the last known elapsed; without that it would lurch forward once every ten seconds. "wave" puts two SVG sine layers on the crest of the fill, running at different lengths and speeds — the mismatch between them is what reads as liquid. "simple" leaves the plain line and builds no SVG and no animation at all.

    With the bargraph on, "simple" is the better pairing: the wave and the bars fight over the same 16 px band at the bottom of the dock. Keep "wave" for docks with visualizer: false.

    The bar is clipped by a frame that inherits the dock's own border-radius, so it follows the corner in every visual style — including the 999 px capsule of liquid, where insetting the bar by the radius would have collapsed it to nothing.

    In the generator this is the "Efeitos do player" section, which writes theme: { visualizer, progress } into content.js. That is only a fallback: config.js always wins, because a player embedded on someone else's site has no content.js at all.

  • Visual style (5 design languages): the whole thing — site and player dock — can switch design language with one field: theme: { style: "clay" } in content.js, or the picker at the top of gerador.html, which shows a live miniature of each option and repaints the generator page as you click.

    style Looks like
    glass (default) Frosted glass: blur, translucent borders, floating island dock. The original look — omit the field and nothing changes.
    clay Claymorphism: opaque puffy surfaces, very round corners, no borders, buttons that squish when pressed.
    minimal Flat: 1px hairlines, no shadow or blur, square corners, and the dock goes back to being an edge-to-edge bar.
    liquid Liquid Glass: thicker blur, specular highlight along the edges, capsule-shaped dock and a sheen that crosses it while playing.
    spatial Spatial UI: neutral glass, wide ambient shadows, larger radii, and a dock that floats clear of the bottom edge.

    Everything lives in css/ui-styles.css as a layer of --ui-* tokens whose defaults are the current glass values, applied only when a data-ui attribute is present on <html> — so a site that never sets style (or never loads the file) renders exactly as before. Each style also has its own light-theme palette; the dock always keeps a dark surface, since it floats over the whole page and carries white text. To tune one, edit its token block in css/ui-styles.css — the generator's preview reads the same tokens and follows along.

  • Developer mode (try the 5 styles on the live site): add ?dev=1 to any page URL and a floating 🎨 button appears, listing the five design languages; picking one repaints the site and the dock immediately. It exists because comparing styles otherwise means re-editing content.js and reloading for every one of them. The choice is remembered in localStorage, so it survives seamless navigation and full reloads — the pre-boot script in each page's <head> reads it before the first paint, otherwise the picked style would flash back to the configured one on every page. Turn it off from the panel ("Sair do modo dev") or with ?dev=0; both also release the style override, so the machine can't get stuck on a look that isn't the site's real one. It lives in site.js and injects its own CSS only when enabled, so ordinary visitors download nothing extra — and since it is per browser, nobody else ever sees the button. The panel is deliberately styled with fixed colours rather than site tokens, so it doesn't change shape along with the style you are judging.

  • Site accent color: one field does it — theme: { accent: "#4dd7e0", accentLight: "" } in content.js, or the color picker in gerador.html (live preview). From that single colour site.js derives the button gradient (--site-accent-2), the background glow (--site-glow-1), the text colour used on top of the accent (--site-accent-ink, picked by WCAG luminance so labels stay readable) and the player's starting accent (--accent, until the cover art sets its own). Leave accentLight empty and the light theme darkens the colour only as much as contrast requires.

  • Menu scroll speed: clicking a menu item glides to the section instead of jumping, and theme: { scrollDuration: 1100 } in content.js sets how long the longest trip may take, in milliseconds (default 1100). The actual duration scales with the distance travelled — a nearby section lands in about half a second, the far end of the page takes the full budget — so raise it for a slower, more deliberate glide and lower it for a snappier one. This exists because the CSS scroll-behavior: smooth alone gives you no control over timing: the browser picks it (Chrome spends roughly 300 ms on any distance), which on a long page reads as a hard cut. site.js animates the trip itself instead, easing in and out, and stops immediately if the visitor scrolls by hand mid-flight. Visitors whose system asks for reduced motion always jump straight to the section, whatever the value.

  • Languages (pt / en / es / it): the whole interface switches language from a picker in the header — section titles, menu, weekday tabs, buttons, accessibility labels, the PWA prompts and the player's own controls (Lyrics, History, Stations, Share). Everything lives in js/i18n.js: one dictionary per language, plus data-i18n attributes on the static HTML. Only the chrome is translated — the news, programmes, team names and slides you write in content.js come out exactly as you wrote them, because they're your content, not the frame. A visitor gets their browser's language when it's one of the four, otherwise the theme: { language: "pt" } default from content.js; their pick is remembered in localStorage and wins from then on. Switching language re-renders the page through the player's seamless navigation, so the music doesn't stop. Dates follow the language too (10 de julho de 2026July 10, 2026). To add a fifth language, copy one dictionary block in js/i18n.js, translate the values and add the code to LANGS; any key you miss falls back to Portuguese instead of showing a raw key. Drop js/i18n.js from the pages and the site is byte for byte the Portuguese-only one it was — every t() call carries the original string as its fallback.

  • Hover zoom: theme: { hoverZoom: false } in content.js turns off the growing part of the hover — the cards that swell a little in liquid and spatial, the gallery photo and video thumbnail that zoom under their frame (those two happen in every style), the action buttons, and the dock buttons in spatial. Everything else the hover does stays: each style keeps its own lift, the border still lights up and the shadow still opens, so the card goes on answering the pointer. It works by putting data-hover-zoom="off" on <html>; without the field nothing is emitted and the CSS is byte for byte what it always was. Visitors whose system asks for reduced motion get no growth either way.

  • Colors (player): Customize the player's colors by editing the css/custom.css file.

  • Behavior: Adapt the player by editing js/radioplayer.js (the component). js/main.js is the legacy non-component version, kept for reference.

  • JavaScript API: the component exposes window.RadioPlayer with play(), pause(), toggle(), the audio element and the root DOM node.

Related Projects

More free radio players from the same author:

Project Style
RadioPlayer Full-page player for any stream (free now-playing API, YouTube clip mode)
Radioplayer_api Multi-station player with 3 switchable layouts
RadioPlayer-ZenoRadio Full-page player for Zeno.FM streams (SSE metadata)
metadados The free now playing API (ICY metadata + iTunes + YouTube clips)
bottom-radioplayer-wordpress WordPress plugin wrapper for this project — settings page, no file editing

Support and Contributions

  • If you have any questions or issues, please open an issue in the GitHub repository.
  • Contributions are welcome! Feel free to submit pull requests with improvements, bug fixes, or new features.

About

🎵 Bottom-bar radio player component — the audio NEVER stops while visitors browse your site (seamless navigation). YouTube clip mode + full site demo. Player de rádio fixo no rodapé que não para ao navegar.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages