# How Tabby's Theme System Works at Runtime: Architecture and Implementation

> Discover how Tabby's theme system works at runtime. Explore its reactive architecture, `ThemesService`, `themeChanged$` observable, and real time CSS variable application.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: architecture
- Published: 2026-03-03

---

**Tabby's theme system uses a reactive service architecture where `ThemesService` resolves the active color scheme, emits changes via `themeChanged$` Observable, and applies CSS variables to the DOM and terminal instances in real-time.**

The Eugeny/tabby repository implements a sophisticated, platform-aware theming system built on Angular's dependency injection and RxJS observables. At runtime, this system seamlessly coordinates between the Electron main process, Angular renderer components, and XTerm terminal instances to maintain visual consistency across the entire application.

## Theme Discovery and Registration

Tabby discovers available themes through Angular's provider system. All theme definitions are registered as injectable providers using the `@Inject(Theme)` decorator in the core module. The `ThemesService` receives these definitions as an array in its constructor via dependency injection.

In [`tabby-core/src/services/themes.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/themes.service.ts), the service initialization looks like this:

```typescript
@Inject(Theme) private themes: Theme[]

```

Each `Theme` object conforms to the `NewTheme` interface defined in [`tabby-core/src/api/theme.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/theme.ts). These are plain data objects that specify the theme's metadata, color palettes, and variant schemes (such as dark and light modes).

## Active Theme Resolution

When determining which theme to display, `ThemesService._getActiveColorScheme()` executes a priority-based selection algorithm. First, it retrieves the user's selected theme from the application configuration. If that theme defines multiple color schemes (for example, separate dark and light variants), the service automatically selects the variant matching the platform's current dark-mode state by calling `platform.isDarkMode()`.

The resolved theme is returned as a `TerminalColorScheme` object containing `background`, `foreground`, and an array of 16 ANSI colors that drive terminal rendering.

## Reactive Notification System

The runtime theme propagation relies on RxJS observables. `ThemesService` exposes `themeChanged$`, an `Observable<Theme>` that components subscribe to for real-time updates. The service listens to platform-level theme changes through `platform.themeChanged$` and recomputes the active scheme whenever the OS signals a mode switch.

As implemented in [`tabby-core/src/services/themes.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/themes.service.ts):

```typescript
get themeChanged$(): Observable<Theme> { return this.themeChanged; }

platform.themeChanged$.subscribe(() => {
    const theme = this._getActiveColorScheme();
    this.applyThemeToRootVariables(theme);
    this.themeChanged.next(theme);
});

```

This reactive pattern ensures that every component receives the new theme simultaneously without manual polling or direct coupling to the platform service.

## Applying Themes to the UI

Once a theme change is detected, the system applies styling through three distinct channels.

### CSS Variables Injection

The `ThemesService.applyThemeToRootVariables()` method writes theme values as CSS custom properties to `document.documentElement`. These `--theme-*` variables control Bootstrap components, window backgrounds, text colors, and scrollbar styling throughout the application. The method constructs a comprehensive `vars` object mapping theme properties to CSS variable names.

### Terminal Frontend Synchronization

Terminal instances require specialized color handling. In [`tabby-terminal/src/frontends/xtermFrontend.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/frontends/xtermFrontend.ts), the XTerm frontend subscribes to `themeChanged$` and constructs an XTerm-specific theme object:

```typescript
const appColorScheme = this.themes._getActiveColorScheme() as TerminalColorScheme;
const theme: ITheme = {
    background: getTerminalBackgroundColor(this.configService, this.themes, scheme) ?? '#00000000',
};
for (let i = 0; i < COLOR_NAMES.length; i++) {
    theme[COLOR_NAMES[i]] = scheme.colors[i];
}
this.xterm.options.theme = theme;

```

This mapping ensures terminal ANSI colors remain consistent with the application's UI palette.

### Electron Window Integration

The Electron main process in [`tabby-electron/src/services/platform.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-electron/src/services/platform.service.ts) detects OS-level theme changes and emits `themeChanged` events to the renderer. The renderer process uses these signals to adjust window chrome elements, such as macOS window button insets (`theme.macOSWindowButtonsInsetX`), ensuring native controls match the selected aesthetic.

## User Interaction and Theme Switching

The settings interface allows manual theme selection through [`appearanceSettingsTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/appearanceSettingsTab.component.ts) in the settings module. When a user selects a different theme, the component invokes `themes.setCurrentTheme(name)`, which triggers the complete reactive flow: resolution of the new color scheme, CSS variable updates, and notification of all subscribers.

To subscribe to theme changes in a custom component:

```typescript
import { Component, OnInit } from '@angular/core';
import { ThemesService } from 'tabby-core';

@Component({
    selector: 'my-widget',
    template: `<div class="my-widget">...</div>`
})
export class MyWidgetComponent implements OnInit {
    constructor(private themes: ThemesService) {}

    ngOnInit(): void {
        this.themes.themeChanged$.subscribe(theme => {
            console.log('New theme active:', theme.name);
        });
    }
}

```

## Summary

- **Tabby's theme system** relies on `ThemesService` in [`tabby-core/src/services/themes.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/themes.service.ts) as the central authority for theme resolution and distribution.
- **Theme registration** occurs through Angular dependency injection, with all themes implementing the `NewTheme` interface defined in [`tabby-core/src/api/theme.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/theme.ts).
- **Runtime resolution** considers both user preferences and platform dark-mode status via `_getActiveColorScheme()`, returning a `TerminalColorScheme` with 16 ANSI colors.
- **Reactive propagation** uses the `themeChanged$` Observable to push updates to all subscribers, including terminal frontends and UI components.
- **Visual application** happens through CSS variables injected into the document root and direct XTerm theme object updates in [`tabby-terminal/src/frontends/xtermFrontend.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/frontends/xtermFrontend.ts).

## Frequently Asked Questions

### How does Tabby detect operating system dark mode changes?

The Electron platform service in [`tabby-electron/src/services/platform.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-electron/src/services/platform.service.ts) monitors the OS appearance settings and exposes `platform.themeChanged$`. `ThemesService` subscribes to this observable and calls `_getActiveColorScheme()` to select the appropriate light or dark variant when the system theme changes.

### Can I access the current terminal color scheme programmatically?

Yes. Inject `ThemesService` and call `themes._getActiveColorScheme()` to retrieve the current `TerminalColorScheme` object. This object contains `background`, `foreground`, and the `colors` array mapping to standard ANSI terminal color indices.

### How do custom components receive theme updates automatically?

Components should inject `ThemesService` and subscribe to `themeChanged$` in their initialization lifecycle (e.g., `ngOnInit`). This Observable emits the new `Theme` object whenever the active scheme changes, allowing components to recalculate colors or trigger redraws without manual refresh logic.

### Where does Tabby store the list of available themes?

Available themes are provided through Angular's `Theme` injection token at application bootstrap. The `ThemesService` constructor receives the complete `Theme[]` array via `@Inject(Theme) private themes: Theme[]`, making all registered themes immediately available for selection and resolution.