Skip to content

Instantly share code, notes, and snippets.

@sanoojes
Last active August 22, 2025 18:10
Show Gist options
  • Select an option

  • Save sanoojes/76fe0d1eac0820327b77e58dcc068e40 to your computer and use it in GitHub Desktop.

Select an option

Save sanoojes/76fe0d1eac0820327b77e58dcc068e40 to your computer and use it in GitHub Desktop.
Register custom React pages in the Spotify client with Spicetify API.

createPage Helper

The createPage function allows you to register a custom React page in the Spotify client (via Spicetify). It handles:

  • Mounting/unmounting your React component into Spotify’s DOM.
  • Navigating to your page using Spotify’s internal History.
  • Returning navigation helpers (goToPage, goBack) so you can trigger page transitions programmatically.
View `createPage.ts` source
import React, { createElement, type ReactNode } from "react";
import { type Root, createRoot } from "react-dom/client";

type HistoryLocation = {
  pathname: string;
};

type PlatformHistory = {
  location: HistoryLocation;
  entries: HistoryLocation[];
  push: (location: string) => void;
  listen: (cb: (location: HistoryLocation | undefined) => void) => void;
};

type CreatePageProps = {
    pathname: string;
    container: ReactNode;
};

const History: PlatformHistory | undefined = Spicetify?.Platform?.History;
const createPage = ({ pathname, container }: CreatePageProps) => {
  const urlPathname = `/${pathname}/`;
  let lastPageLocation: string | null = null;

  const rootId = `root-${pathname}`;
  let reactRoot: Root | null = null;

  const root = document.createElement("div");
  root.id = rootId;

  const addToDom = async () => {
    const parent = await waitForElement(
      ".main-view-container__scroll-node div[data-overlayscrollbars-viewport]"
    );
    if (!parent.querySelector(`#${rootId}`)) {
      parent.appendChild(root);
    }
  };

  const mount = async () => {
    if (reactRoot) return;
    reactRoot = createRoot(root);
    reactRoot.render(container);
    await addToDom();
  };

  const unmount = () => {
    if (!reactRoot) return;
    reactRoot.unmount();
    reactRoot = null;
    root.remove();
  };

  const handlePageChange = (currentLocation: HistoryLocation | undefined) => {
    if (!History || !currentLocation) return;

    const lastEntry = History.entries.at(-2);
    lastPageLocation = lastEntry?.pathname ?? "/";

    if (currentLocation.pathname === urlPathname) {
      void mount();
    } else {
      unmount();
    }
  };

  handlePageChange(History?.location);
  History?.listen(handlePageChange);

  const goToPage = () => History?.push(urlPathname);
  const goBack = () => History?.push(lastPageLocation ?? "/");

  return { goToPage, goBack };
};

function waitForElement(
  selector: string,
  { timeout = 3000 }: { timeout?: number } = {}
): Promise<Element> {
  const startTime = performance.now();

  return new Promise((resolve, reject) => {
    function check(): void {
      const element = document.querySelector(selector);

      if (element) {
        resolve(element);
        return;
      }

      if (performance.now() - startTime > timeout) {
        reject(null);
        console.warn(`Timeout: Could not find element: ${selector}`);
        return;
      }

      requestAnimationFrame(check);
    }

    check();
  });
}

Example Usage

const Page = () => <h1>Hello, world!</h1>;

const { goToPage, goBack } = createPage({
  pathname: "my-custom-page",
  container: <Page />,
});

// Example: open/close buttons
document.body.append(
  Object.assign(document.createElement("button"), {
    innerText: "Open Page",
    onclick: goToPage,
  }),
  Object.assign(document.createElement("button"), {
    innerText: "Close Page",
    onclick: goBack,
  })
);

How It Works

  1. Creates a root container (<div id="root-{pathname}" />) for your page.
  2. Waits for Spotify’s main DOM container (inside .main-view-container__scroll-node) to become available.
  3. Mounts your React component into that container when the Spotify history matches your custom pathname.
  4. Unmounts your component when navigating away (preventing memory leaks).
  5. Provides:
    • goToPage: Pushes your custom pathname into Spotify’s history (loads your page).
    • goBack: Pushes the previous history entry (returns to where you were).

Notes & Gotchas

  • pathname should be unique (e.g. "my-lyrics-viewer-page"), or it may conflict with other pages.
  • The component is mounted only when the page is active – don’t store state globally in it if you want persistence across navigations.

Note: This documentation was generated with the help of AI—because, well, why not?

import { type ReactNode } from "react";
import { type Root, createRoot } from "react-dom/client";
import { waitForElements } from "@utils/dom";
const History: PlatformHistory | undefined = Spicetify?.Platform?.History;
export default function createPage({ pathname, children }: CreatePageProps) {
const urlPathname = `/${pathname}/`;
let lastPageLocation: string | null = null;
const rootId = `root-${pathname}`;
let reactRoot: Root | null = null;
const root = document.createElement("div");
root.id = rootId;
const addToDom = async () => {
const parent = await waitForElements(
".main-view-container__scroll-node div[data-overlayscrollbars-viewport]"
);
if (!parent.querySelector(`#${rootId}`)) {
parent.appendChild(root);
}
};
const mount = async () => {
if (reactRoot) return;
reactRoot = createRoot(root);
reactRoot.render(children);
await addToDom();
};
const unmount = () => {
if (!reactRoot) return;
reactRoot.unmount();
reactRoot = null;
root.remove();
};
const handlePageChange = (currentLocation: HistoryLocation | undefined) => {
if (!History || !currentLocation) return;
const lastEntry = History.entries.at(-2);
lastPageLocation = lastEntry?.pathname ?? "/";
if (currentLocation.pathname === urlPathname) {
void mount();
} else {
unmount();
}
};
handlePageChange(History?.location);
History?.listen(handlePageChange);
const goToPage = () => History?.push(urlPathname);
const goBack = () => History?.push(lastPageLocation ?? "/");
return { goToPage, goBack };
}
export type HistoryLocation = {
pathname: string;
};
export type PlatformHistory = {
location: HistoryLocation;
entries: HistoryLocation[];
push: (location: string) => void;
listen: (cb: (location: HistoryLocation | undefined) => void) => void;
};
export type CreatePageProps = {
pathname: string;
children: ReactNode;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment