Skip to main content

Theme System

LiquidBounce features a powerful theme system built on Svelte and served through an integrated browser backend. Themes control the appearance of menus, HUD, and all UI elements.

Overview

The theme system provides:

Svelte UI

Modern reactive UI framework with component-based architecture

Multiple Sources

Load themes from resources, local files, or marketplace

Custom Backgrounds

Support for image backgrounds and GLSL shaders

Hot Reload

Themes reload on resource manager refresh

Theme Manager

The ThemeManager (line 47) handles all theme operations:
object ThemeManager : Config("theme") {
    internal val themesFolder = File(ConfigSystem.rootFolder, "themes")
    
    val themes: List<Theme>
    val themeIds get() = themes.map { it.metadata.id }
    
    private var currentTheme by text("Theme", "liquidbounce")
    internal var includedTheme: Theme? = null
    private var temporaryTheme: Theme? = null
}

Theme Origins

Themes can come from three sources, loaded in priority order:
1

Local Themes

Themes in .liquidbounce/themes/ directory (highest priority)
Theme.load(Theme.Origin.LOCAL, file.relativeTo(themesFolder))
2

Marketplace Themes

Themes installed from marketplace
MarketplaceManager.getSubscribedItemsOfType(MarketplaceItemType.THEME)
    .forEach { item ->
        Theme.load(Theme.Origin.MARKETPLACE, relativeFile)
    }
3

Resource Theme

Built-in “liquidbounce” theme (fallback)
includedTheme = Theme.load(Theme.Origin.RESOURCE, File("liquidbounce"))
If multiple themes have the same ID, only the first (highest priority) is loaded.

Loading Themes

Themes are loaded during client initialization:
suspend fun init() {
    // Load default theme
    includedTheme = Theme.load(Theme.Origin.RESOURCE, File("liquidbounce"))
}

suspend fun load() {
    themes.clear()
    
    // Load local themes
    themesFolder.listFiles { it.isDirectory }?.forEach { file ->
        Theme.load(Theme.Origin.LOCAL, file.relativeTo(themesFolder))
            .addIfUnloaded()
    }
    
    // Load marketplace themes
    MarketplaceManager.getSubscribedItemsOfType(MarketplaceItemType.THEME)
        .forEach { item ->
            Theme.load(Theme.Origin.MARKETPLACE, relativeFile)
                .addIfUnloaded()
        }
    
    // Update UI
    ModuleHud.updateThemes()
    ScreenManager.update()
}

Theme Selection

The active theme is determined by:
var theme: Theme?
    get() = temporaryTheme
        ?: themes.find { it.metadata.id.equals(currentTheme, true) }
        ?: includedTheme
    set(value) {
        if (value?.origin?.external == true) {
            temporaryTheme = value  // External theme
        } else {
            temporaryTheme = null
            currentTheme = value.metadata.id
        }
    }
Temporary themes (external) take precedence but aren’t persisted to config.

Svelte Integration

Themes are built with Svelte and served through the browser backend:

Theme Structure

theme/
├── metadata.json       # Theme information
├── src/
│   ├── App.svelte      # Root component
│   ├── routes/         # Route components
│   │   ├── menu/
│   │   │   ├── title/Title.svelte
│   │   │   ├── multiplayer/Multiplayer.svelte
│   │   │   └── altmanager/AltManager.svelte
│   │   └── hud/
│   ├── integration/    # LiquidBounce integration
│   │   ├── ws.ts       # WebSocket communication
│   │   ├── rest.ts     # REST API
│   │   └── events.ts   # Event handling
│   └── theme/
│       └── theme_config.ts
└── public/
    └── img/            # Theme assets

Theme Configuration

Themes define their configuration in TypeScript:
// theme_config.ts
export let spaceSeperatedNames = writable(false);

export function convertToSpacedString(name: string): string {
    const regex = /[A-Z]?[a-z]+|[0-9]+|[A-Z]+(?![a-z])/g;
    return (name.match(regex) as string[]).join(" ");
}

Browser Integration

Themes are rendered in an embedded browser:

Opening a Theme Screen

fun openImmediate(
    customScreenType: CustomScreenType? = null,
    markAsStatic: Boolean = false,
    settings: BrowserSettings
): Browser {
    val backend = BrowserBackendManager.backend
        ?: error("Browser backend is not initialized.")
    
    return backend.createBrowser(
        getScreenLocation(customScreenType, markAsStatic).url,
        settings = settings
    )
}

Input-Aware Screens

For interactive screens (menus, click GUI):
fun openInputAwareImmediate(
    customScreenType: CustomScreenType? = null,
    markAsStatic: Boolean = false,
    settings: BrowserSettings,
    priority: Short = 10,
    inputAcceptor: InputAcceptor = takesInputHandler
): Browser
Input-aware screens run at higher refresh rates for smooth interaction.

Screen Routes

Themes define routes for different screens:
fun getScreenLocation(
    customScreenType: CustomScreenType? = null,
    markAsStatic: Boolean = false
): ScreenLocation {
    val theme = theme.takeIf { theme ->
        customScreenType == null || 
        theme?.isSupported(customScreenType.routeName) == true
    } ?: includedTheme
    
    return ScreenLocation(
        theme,
        theme.getUrl(customScreenType?.routeName, markAsStatic)
    )
}
Common routes include:
  • title - Main menu
  • multiplayer - Server list
  • altmanager - Account manager
  • hud - In-game HUD
  • clickgui - Click GUI

Backgrounds

Themes can provide custom backgrounds:

Background Images

fun loadBackgroundAsync(): CompletableFuture<Unit> = renderScope.future {
    theme?.loadBackgroundImage()
}

Shader Backgrounds

Themes can include GLSL shaders:
var shaderEnabled by boolean("Shader", false)
    .onChange { enabled ->
        if (enabled) {
            renderScope.launch {
                theme?.compileShader()
                includedTheme?.compileShader()
            }
        }
        return@onChange enabled
    }
Rendering backgrounds:
fun drawBackground(
    context: GuiGraphics,
    width: Int, height: Int,
    mouseX: Int, mouseY: Int,
    delta: Float
): Boolean {
    val background = if (shaderEnabled) {
        theme?.backgroundShader
    } else {
        theme?.backgroundImage
    } ?: return false
    
    background.draw(context, width, height, mouseX, mouseY, delta)
    return true
}

Resource Reloading

Themes support hot-reloading:
internal val reloader = ResourceManagerReloadListener { resourceManager ->
    themes.forEach { it.onResourceManagerReload(resourceManager) }
    logger.info("Reloaded ${themes.size} themes.")
}
This is triggered when:
  • Resource packs are reloaded
  • F3+T is pressed
  • Themes are modified during development

Theme Metadata

Each theme includes a metadata.json file:
{
  "id": "liquidbounce",
  "name": "LiquidBounce Default",
  "version": "1.0.0",
  "author": "CCBlueX",
  "description": "Default LiquidBounce theme",
  "routes": [
    "title",
    "multiplayer",
    "altmanager",
    "hud",
    "clickgui"
  ]
}

Creating Custom Themes

To create a custom theme:
1

Set Up Structure

Create a directory in .liquidbounce/themes/ with the required structure
2

Define Metadata

Create metadata.json with theme information and supported routes
3

Build UI

Create Svelte components for each route
4

Integrate with LiquidBounce

Use the integration modules (ws.ts, rest.ts, events.ts) to communicate with the client
5

Test and Load

Reload themes via resource reload or restart the client

Code References

ThemeManager.kt

Theme management - line 47
integration/theme/ThemeManager.kt

src-theme/

Default theme source code
src-theme/src/

integration/

Svelte integration modules
src-theme/src/integration/

theme_config.ts

Theme configuration
src-theme/src/theme/theme_config.ts

Best Practices

1

Support Core Routes

Ensure your theme supports at least title, multiplayer, and hud routes
2

Use Integration APIs

Leverage the provided WebSocket and REST APIs for client communication
3

Optimize Assets

Keep theme assets small for faster loading
4

Test Hot Reload

Verify your theme reloads correctly with F3+T
5

Handle Fallbacks

Gracefully handle missing data from the client

Build docs developers (and LLMs) love