Skip to content

Theme Plugin Guide

60plus edited this page Apr 13, 2026 · 1 revision

Theme Plugin Guide

Theme plugins provide complete custom layouts - not just color skins. You can replace the entire navbar, home page, library views, and couch mode.

Architecture

A theme plugin provides three hooks:

Hook Returns Purpose
frontend_get_theme() dict Theme definition (skins, settings, layout ID)
frontend_get_css() str CSS overrides for GD components
frontend_get_js() str Dynamic JS effects (particles, glass blur, etc.)

Vue SFC Compilation

Theme plugins can include .vue files that are compiled on container startup:

  1. Put .vue files in your plugin root
  2. Name them with your theme prefix: MyThemeLayout.vue, MyThemeCouch.vue
  3. On container start, Vite compiles them into layout.js + layout.css
  4. Frontend auto-loads and registers them

Component Naming Convention

Layout ID Layout Component Couch Component Children
my-theme MyThemeLayout.vue MyThemeCouch.vue MyThemeHome.vue, MyThemeLibrary.vue, etc.
neon-horizon NeonHorizonLayout.vue NeonHorizonCouch.vue NeonHorizonHome.vue, etc.

Children are imported inside Layout/Couch - the compiler bundles everything together.

Important: After installing/updating a theme plugin, restart the container (Plugin Store has a "Restart Now" button).

Theme Definition

@hookimpl
def frontend_get_theme(self):
    return {
        "id": "my-theme",
        "name": "My Theme",
        "description": "A cool theme",
        "layout": "my-theme",           # Must match Vue component naming
        "skins": [
            {"id": "blue", "name": "Ocean", "preview": "#2563eb"},
            {"id": "sunset", "name": "Sunset", "preview": "linear-gradient(135deg, #f97316, #ec4899)", "dual": True},
        ],
        "defaultSkin": "blue",
        "cssFile": "my-theme",           # Base name for CSS
        "font": "https://fonts.googleapis.com/css2?family=MyFont&display=swap",
        "previewHtml": "<div>...</div>",  # Optional: preview in theme switcher
        "settings": [ ... ],             # See Settings section below
    }

Skins

Skins define color palettes. Each skin sets CSS variables via data-skin attribute:

  • Solid skins: {"id": "blue", "name": "Ocean", "preview": "#2563eb"}
  • Gradient skins: {"id": "sunset", "name": "Sunset", "preview": "linear-gradient(...)", "dual": True}

Your CSS uses these selectors:

:root[data-skin="blue"] {
  --pl: #2563eb;        /* Primary color */
  --pl-light: #60a5fa;  /* Light variant */
  --pglow: rgba(37, 99, 235, .4);
  --bg: #0a0a1a;        /* Background */
}

Settings

Settings appear in Settings > Appearance > Theme Settings:

"settings": [
    {
        "key": "particleCount",
        "label": "nh.setting_particles",      # i18n key
        "hint": "nh.setting_particles_hint",   # i18n key
        "type": "select",
        "default": "6",
        "options": ["0", "3", "6", "12"],
        "optionLabels": ["nh.opt_none", "nh.opt_few", "nh.opt_normal", "nh.opt_many"],
        "cssVar": "--nh-particle-count",
    },
    {
        "key": "glassBlur",
        "label": "Glass Blur",                 # Plain string also works
        "type": "range",
        "default": 20,
        "min": 0, "max": 60, "step": 1,
        "unit": "px",                          # Appended to value: 20 -> "20px"
        "cssVar": "--glass-blur-px",
    },
    {
        "key": "scanlines",
        "label": "Scanlines",
        "type": "toggle",
        "default": False,
        "cssVar": "--my-scanlines",            # "1" when on, "0" when off
    },
]

Important: Theme settings are NOT the same as config_schema in plugin.json. Theme settings go in frontend_get_theme().settings[] and render in Appearance. config_schema renders in Plugins and is for plugin configuration (API keys, toggles, etc.). Theme plugins typically have empty config_schema: {}.

Vue Files

Plugin .vue files compile outside the main app. You cannot use @/ imports. Use window.__GD__:

import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'

const _gd = (window as any).__GD__
const client = _gd.api                        // Axios with Bearer token
const auth = _gd.stores.auth()                 // { user } - token hidden
const themeStore = _gd.stores.theme()          // Full theme store
const t = _gd.i18n?.t || ((k: string) => k)   // i18n translation

// Couch Mode
const { useCouchNav, couchNavPaused } = _gd.composables
const getEjsCore = _gd.getEjsCore             // Platform -> EmulatorJS core

// Notifications
_gd.notifications.add({ id: 'my-alert', count: 1, label: 'Something happened' })

Global components available in templates:

  • <DownloadManager /> - Download queue (admin only)
  • <RandomGamePicker /> - Random game dice
  • <AmbientBackground /> - Animated orb background

CSS Patterns

data-theme selector

Scope all your CSS with data-theme to avoid conflicts:

[data-theme="my-theme"] .some-class {
  /* Only applies when your theme is active */
}

[data-theme="my-theme"][data-skin="sunset"] .accent {
  /* Specific skin + theme combination */
}

CSS Variables

GD defines standard CSS vars that all themes should use:

  • --pl - Primary color
  • --pl-light - Light primary
  • --pglow / --pglow2 - Glow effects
  • --bg / --bg2 / --bg3 - Background layers
  • --text / --muted - Text colors
  • --glass-bg / --glass-border - Glass panel styles

JS Patterns (frontend_get_js)

JS returned by frontend_get_js() runs as raw JavaScript (not a module). Use IIFE pattern:

(function() {
  const THEME_ID = 'my-theme';
  const root = document.documentElement;
  
  function isActive() {
    return root.getAttribute('data-theme') === THEME_ID;
  }
  
  // React to theme setting changes
  root.addEventListener('gd-theme-updated', () => {
    if (!isActive()) return;
    // Read CSS vars and apply effects
    const blur = parseFloat(getComputedStyle(root).getPropertyValue('--glass-blur-px')) || 20;
  });
  
  // Watch for DOM changes (new elements need styling)
  new MutationObserver(() => {
    if (!isActive()) return;
    // Apply dynamic styles
  }).observe(document.body, { childList: true, subtree: true });
})();

Plugin Assets

Static files in assets/ are served at /api/plugins/{id}/assets/{path}:

const PLUGIN_ID = 'my-theme'
function pluginAsset(path: string): string {
  return `/api/plugins/${PLUGIN_ID}/assets/${path}`
}

// Images
pluginAsset('icons/snes.png')

// JSON data files
const { data } = await client.get(`/api/plugins/${PLUGIN_ID}/assets/metadata.json`)

Couch Mode

Couch Mode components use gamepad/keyboard navigation via useCouchNav:

const { useCouchNav, couchNavPaused } = _gd.composables

useCouchNav({
  left:    () => { /* D-pad left */ },
  right:   () => { /* D-pad right */ },
  up:      () => { /* D-pad up */ },
  down:    () => { /* D-pad down */ },
  confirm: () => { /* A button / Enter */ },
  back:    () => { /* B button / Escape */ },
  menu:    () => { /* Start button / M key */ },
})

// Pause nav during overlays
couchNavPaused.value = true
// Resume
couchNavPaused.value = false

Launch games with EmulatorJS:

const core = _gd.getEjsCore(platform.fs_slug)  // Returns core name or null
if (core) {
  window.open(`/player.html?rom_id=${rom.id}&ejs_core=${core}&platform=${platform.fs_slug}`)
}

Clone this wiki locally