How Lepton Manages Theme Switching Between Light and Dark Modes
Lepton manages theme switching through a centralized ThemeManager class that reads JSON theme definitions and applies them as CSS custom properties to the :root element, with the user's preference persisted in a global configuration store.
Lepton, the open-source GitHub gist desktop client, implements a robust theme switching system that allows seamless transitions between light and dark visual modes. Understanding how Lepton handles theme switching reveals an elegant architecture combining JSON configuration files, CSS custom properties, and a singleton manager pattern. This article breaks down the complete implementation based on the latest source code from the hackjutsu/Lepton repository.
Understanding Lepton's Theme Architecture
The theme system rests on three foundational pillars: static JSON definitions, a runtime manager, and persistent configuration storage.
In app/utilities/themeManager/themes/, two files—lightTheme.json and darkTheme.json—define the CSS custom property values for each palette. These properties include keys like bg-primary and text-primary that map to specific color values.
The ThemeManager class, located in app/utilities/themeManager/index.js, serves as the runtime engine. It reads the selected theme definition and injects those values into the document's root element as CSS variables.
Finally, the system relies on nconf to persist user preferences. When the application initializes in main.js, the configuration from configs/defaultConfig.js loads globally as global.conf, making the stored theme value accessible throughout the app via conf.get('theme').
Theme Definition Files (JSON)
The static theme definitions reside in JSON files within the themes directory. Each file contains a flat mapping of CSS variable names to color hex codes.
For example, lightTheme.json and darkTheme.json both define the same set of keys—such as bg-primary, text-primary, and border colors—but assign different values appropriate to each mode. This ensures visual consistency across the application while allowing complete palette swapping.
The ThemeManager Implementation
The core logic lives in app/utilities/themeManager/index.js. This singleton class exposes two primary methods: setTheme(themeName) and toggleTheme().
When setTheme() receives a theme name (either "light" or "dark"), it forwards the call to generateScheme(). This private method loads the corresponding JSON file and iterates over each key-value pair, calling document.documentElement.style.setProperty('--key', value) for every entry. By mutating the CSS variable map on the :root element, the manager triggers an instantaneous global style update without requiring component re-renders.
The toggleTheme() method alternates the internal currentTheme state between "light" and "dark" before invoking generateScheme(). While this API exists for programmatic use, note that it is not currently wired to a UI element in the main interface.
Configuration and Initialization Flow
The theme application follows a specific initialization sequence when Lepton starts:
- Configuration Loading: In
main.js,nconfloadsconfigs/defaultConfig.jsand exposes it globally asglobal.conf. - Manager Instantiation: In
app/containers/appContainer/index.js, the application creates a singleton instance:const themeManager = new ThemeManager(). - Theme Application: Immediately following instantiation, the container calls
themeManager.setTheme(conf.get('theme')), retrieving the persisted user preference and applying it before the UI renders.
This flow ensures that the correct theme appears immediately upon application launch, preventing flash-of-unstyled-content or visual flickering between modes.
Consuming Themes in Components
Components reference the theme variables through standard CSS var() functions. In app/containers/snippet/index.scss, for instance, styles declare:
.snippet-card {
background-color: var(--bg-primary);
color: var(--text-primary);
}
Because the ThemeManager updates the :root element directly, all components using these variables reflect changes instantly without JavaScript intervention.
Certain components require asset adaptation beyond CSS variables. The About page (app/containers/aboutPage/index.js) and code editor components (gistEditor and codeArea) read conf.get('theme') at render time to select appropriate image assets or CodeMirror highlight themes (such as switching between one-dark and github highlighting).
Practical Implementation Examples
To manually apply a theme in a component or utility:
import ThemeManager from '../../utilities/themeManager'
const manager = new ThemeManager()
// Apply dark theme explicitly
manager.setTheme('dark')
To toggle themes programmatically (useful for keyboard shortcuts or menu items):
// Assuming manager was instantiated earlier
function onThemeShortcut () {
manager.toggleTheme()
}
For dynamic asset selection based on the current configuration:
const highlightTheme = conf.get('theme') === 'dark' ? 'one-dark' : 'github'
Summary
- Lepton theme switching relies on JSON definition files (
lightTheme.jsonanddarkTheme.json) that map CSS custom property names to color values. - The ThemeManager class in
app/utilities/themeManager/index.jsapplies themes by setting variables on the:rootelement viadocument.documentElement.style.setProperty(). - User preferences persist in
configs/defaultConfig.jsand load globally throughnconfinmain.js. - The AppContainer initializes the manager and applies the stored theme during application startup.
- UI components consume themes through CSS
var()functions, enabling instant updates without re-renders. - Certain components dynamically select assets by reading
conf.get('theme')directly.
Frequently Asked Questions
Where does Lepton store theme definitions?
Lepton stores theme definitions in two JSON files located at app/utilities/themeManager/themes/lightTheme.json and darkTheme.json. These files contain key-value pairs mapping CSS variable names (like bg-primary) to hex color codes. The ThemeManager class reads these files at runtime to populate the CSS custom properties on the document root.
How does Lepton apply the selected theme without reloading the application?
The ThemeManager uses CSS custom properties (variables) injected into the :root element. When setTheme() is called, it iterates over the selected JSON file and calls document.documentElement.style.setProperty('--key', value) for each entry. Because components reference these variables via var(--key) in their SCSS files, the UI updates instantly without requiring a page reload or component remount.
Can I programmatically toggle between light and dark modes in Lepton?
Yes. The ThemeManager class exposes a toggleTheme() method in app/utilities/themeManager/index.js that switches the internal state between "light" and "dark" and regenerates the color scheme. While this method is not currently connected to a UI button in the main interface, the API is available for custom keyboard shortcuts or menu implementations.
How does Lepton remember the user's theme preference across sessions?
The application uses nconf to persist configuration data. When a user selects a theme, the value is stored in the global configuration object (defined in configs/defaultConfig.js and loaded in main.js). On application startup, app/containers/appContainer/index.js reads this value using conf.get('theme') and passes it to the ThemeManager before the UI renders.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →